zernio-cli 0.4.0 → 0.5.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/README.md +13 -1
- package/claude/plugin.json +2 -2
- package/claude/skills/zernio/SKILL.md +47 -37
- package/claude/skills/zernio/references/zernio-api-surface.md +92 -35
- package/claude/skills/zernio/references/zernio-best-practices.md +80 -20
- package/claude/skills/zernio/references/zernio-workflows.md +248 -19
- package/dist/commands/posts-create.d.ts +2 -0
- package/dist/commands/posts-create.js +110 -0
- package/dist/commands/posts.js +3 -63
- package/dist/utils/posts-create-diagnostics.d.ts +22 -0
- package/dist/utils/posts-create-diagnostics.js +83 -0
- package/dist/utils/posts-create-platform-data.d.ts +27 -0
- package/dist/utils/posts-create-platform-data.js +103 -0
- package/dist/utils/posts-create-thread-items.d.ts +1 -0
- package/dist/utils/posts-create-thread-items.js +83 -0
- package/dist/utils/posts-create-validation-error.d.ts +5 -0
- package/dist/utils/posts-create-validation-error.js +10 -0
- package/docs/cli.md +57 -0
- package/docs/development-roadmap.md +4 -3
- package/docs/project-changelog.md +4 -0
- package/package.json +1 -1
|
@@ -1,58 +1,287 @@
|
|
|
1
1
|
# Zernio Workflows
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use these flows as recipes. Replace IDs with values from discovery commands.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Setup and Health
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
zernio auth:login
|
|
8
|
+
zernio auth:login --device-name "$(hostname)"
|
|
9
9
|
zernio auth:check --pretty
|
|
10
|
+
zernio doctor --connection --pretty
|
|
11
|
+
zernio platforms:list --pretty
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
For CI/agents:
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
17
|
export ZERNIO_API_KEY="sk_..."
|
|
16
18
|
zernio doctor --connection --pretty
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
##
|
|
21
|
+
## Discover Workspace IDs
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
zernio profiles:list --pretty
|
|
25
|
+
zernio accounts:list --profileId <profileId> --pretty
|
|
26
|
+
zernio accounts:health --profileId <profileId> --pretty
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When ambiguous, always ask the user which profile/account to use. Do not guess between client brands.
|
|
30
|
+
|
|
31
|
+
## Profile Lifecycle
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
zernio profiles:create --name "Client A" --description "Launch workspace" --color "#2563eb"
|
|
35
|
+
zernio profiles:update <profileId> --name "Client A Social" --isDefault false
|
|
36
|
+
zernio profiles:get <profileId> --pretty
|
|
37
|
+
zernio profiles:delete <profileId>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Delete only after explicit confirmation.
|
|
41
|
+
|
|
42
|
+
## Create Posts
|
|
43
|
+
|
|
44
|
+
Publish now:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
zernio posts:create --text "Hello from Zernio" --accounts <accountId>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Schedule:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
zernio posts:create \
|
|
54
|
+
--text "Launch update" \
|
|
55
|
+
--accounts <accountId1>,<accountId2> \
|
|
56
|
+
--scheduledAt "2026-06-20T09:00:00Z" \
|
|
57
|
+
--timezone "America/New_York"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Draft:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
zernio posts:create --text "Draft copy" --accounts <accountId> --draft
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Native X/Twitter thread:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
zernio posts:create \
|
|
70
|
+
--text "Thread display title" \
|
|
71
|
+
--accounts <twitterAccountId> \
|
|
72
|
+
--threadJson '["tweet 1","tweet 2","tweet 3"]'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Thread from file:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
zernio posts:create \
|
|
79
|
+
--text "Thread display title" \
|
|
80
|
+
--accounts <twitterAccountId> \
|
|
81
|
+
--threadFile ./thread.txt
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`thread.txt` can be a JSON array or plain text separated by lines containing only `---`. When thread items are present, `--text` is only for Zernio display/search; put the first published tweet in `threadItems[0]`.
|
|
85
|
+
|
|
86
|
+
Native X/Twitter quote or reply:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
zernio posts:create \
|
|
90
|
+
--text "my take" \
|
|
91
|
+
--accounts <twitterAccountId> \
|
|
92
|
+
--quoteTweetId "https://x.com/user/status/2061975910467698972"
|
|
93
|
+
|
|
94
|
+
zernio posts:create \
|
|
95
|
+
--text "reply text" \
|
|
96
|
+
--accounts <twitterAccountId> \
|
|
97
|
+
--replyToTweetId "2061975910467698972"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `--platformSpecificData '{"replySettings":"following"}'` for advanced X passthrough. X-specific options require only `twitter`/`x` targets.
|
|
101
|
+
|
|
102
|
+
Track/retry:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
zernio posts:list --status failed --limit 20 --pretty
|
|
106
|
+
zernio posts:get <postId> --pretty
|
|
107
|
+
zernio posts:retry <postId> --pretty
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Debug failed post creation without exposing secrets:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
zernio posts:create --text "Draft copy" --accounts <accountId> --draft --debug-safe --pretty
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Upload Media Then Post
|
|
20
117
|
|
|
21
118
|
```bash
|
|
22
119
|
zernio media:upload ./image.jpg --pretty
|
|
23
120
|
zernio posts:create \
|
|
24
121
|
--text "Launch update" \
|
|
25
122
|
--accounts <accountId> \
|
|
26
|
-
--media "<url-from-upload>"
|
|
123
|
+
--media "<public-url-from-upload>"
|
|
27
124
|
```
|
|
28
125
|
|
|
29
|
-
|
|
126
|
+
For non-post upload endpoints or raw binary endpoints, use `api:call --raw-body-file`.
|
|
30
127
|
|
|
31
128
|
## Queue Scheduling
|
|
32
129
|
|
|
33
|
-
|
|
130
|
+
Let Zernio assign the queue slot:
|
|
34
131
|
|
|
35
132
|
```bash
|
|
36
133
|
zernio api:call createPost \
|
|
37
|
-
--body-json '{"content":"Queued","platforms":[{"platform":"twitter","accountId":"acc_123"}],"queuedFromProfile":"profile_123"}'
|
|
134
|
+
--body-json '{"content":"Queued post","platforms":[{"platform":"twitter","accountId":"acc_123"}],"queuedFromProfile":"profile_123"}' \
|
|
135
|
+
--request-id req_queue_001 \
|
|
136
|
+
--pretty
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Optional `queueId` targets a queue. Do not feed `queue/next-slot` into `scheduledFor`; that endpoint is preview-only.
|
|
140
|
+
|
|
141
|
+
## Analytics
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
zernio analytics:posts --profileId <profileId> --from "2026-06-01" --to "2026-06-14" --pretty
|
|
145
|
+
zernio analytics:daily --platform instagram --pretty
|
|
146
|
+
zernio analytics:best-time --profileId <profileId> --platform linkedin --pretty
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For endpoint-specific metrics such as demographics or ad analytics:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
zernio api:catalog --tag Analytics --search demographics --pretty
|
|
153
|
+
zernio api:describe getInstagramDemographics --pretty
|
|
154
|
+
zernio api:call getInstagramDemographics --query accountId=<accountId> --pretty
|
|
38
155
|
```
|
|
39
156
|
|
|
40
|
-
|
|
157
|
+
## Inbox, Comments, Reviews
|
|
41
158
|
|
|
42
|
-
|
|
159
|
+
```bash
|
|
160
|
+
zernio inbox:conversations --platform instagram --limit 20 --pretty
|
|
161
|
+
zernio inbox:messages <conversationId> --accountId <accountId> --pretty
|
|
162
|
+
zernio inbox:send <conversationId> --accountId <accountId> --message "Thanks for reaching out"
|
|
163
|
+
```
|
|
43
164
|
|
|
44
165
|
```bash
|
|
45
|
-
zernio
|
|
46
|
-
zernio
|
|
47
|
-
zernio
|
|
166
|
+
zernio inbox:comments --accountId <accountId> --limit 20 --pretty
|
|
167
|
+
zernio inbox:post-comments <postId> --accountId <accountId> --pretty
|
|
168
|
+
zernio inbox:reply <postId> --accountId <accountId> --commentId <commentId> --message "Thank you"
|
|
48
169
|
```
|
|
49
170
|
|
|
50
|
-
|
|
171
|
+
```bash
|
|
172
|
+
zernio inbox:reviews --platform googlebusiness --minRating 1 --maxRating 3 --pretty
|
|
173
|
+
zernio inbox:review-reply <reviewId> --accountId <accountId> --message "Thanks for the feedback"
|
|
174
|
+
```
|
|
51
175
|
|
|
52
|
-
##
|
|
176
|
+
## Contacts and CRM
|
|
53
177
|
|
|
54
178
|
```bash
|
|
55
|
-
zernio
|
|
56
|
-
zernio
|
|
57
|
-
zernio
|
|
179
|
+
zernio contacts:list --search "john" --tag vip --pretty
|
|
180
|
+
zernio contacts:create --profileId <profileId> --name "John Doe" --email john@example.com
|
|
181
|
+
zernio contacts:update <contactId> --tags "vip,lead" --isSubscribed true
|
|
182
|
+
zernio contacts:channels <contactId> --pretty
|
|
183
|
+
zernio contacts:set-field <contactId> lifecycle_stage --value lead
|
|
184
|
+
zernio contacts:clear-field <contactId> lifecycle_stage
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Bulk import:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
zernio contacts:bulk-create --profileId <profileId> --accountId <accountId> --platform instagram --file ./contacts.json
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Broadcasts
|
|
194
|
+
|
|
195
|
+
Direct message broadcast:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
zernio broadcasts:create \
|
|
199
|
+
--profileId <profileId> \
|
|
200
|
+
--accountId <accountId> \
|
|
201
|
+
--platform instagram \
|
|
202
|
+
--name "Launch" \
|
|
203
|
+
--message "We just launched"
|
|
204
|
+
zernio broadcasts:add-recipients <broadcastId> --contactIds <id1>,<id2>
|
|
205
|
+
zernio broadcasts:send <broadcastId>
|
|
58
206
|
```
|
|
207
|
+
|
|
208
|
+
WhatsApp template broadcast:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
zernio broadcasts:create \
|
|
212
|
+
--profileId <profileId> \
|
|
213
|
+
--accountId <accountId> \
|
|
214
|
+
--platform whatsapp \
|
|
215
|
+
--name "Order update" \
|
|
216
|
+
--templateName order_confirmation \
|
|
217
|
+
--templateLanguage en
|
|
218
|
+
zernio broadcasts:schedule <broadcastId> --scheduledAt "2026-06-20T10:00:00Z"
|
|
219
|
+
zernio broadcasts:recipients <broadcastId> --status delivered --pretty
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Sequences
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
zernio sequences:create \
|
|
226
|
+
--profileId <profileId> \
|
|
227
|
+
--accountId <accountId> \
|
|
228
|
+
--platform instagram \
|
|
229
|
+
--name "Welcome Series" \
|
|
230
|
+
--stepsFile ./steps.json
|
|
231
|
+
zernio sequences:activate <sequenceId>
|
|
232
|
+
zernio sequences:enroll <sequenceId> --contactIds <contactId1>,<contactId2>
|
|
233
|
+
zernio sequences:enrollments <sequenceId> --status active --pretty
|
|
234
|
+
zernio sequences:pause <sequenceId>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Use `stepsFile` for multi-step JSON. Prefer `api:call updateSequence --body-file` for complex edits.
|
|
238
|
+
|
|
239
|
+
## Comment-to-DM Automations
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
zernio automations:create \
|
|
243
|
+
--profileId <profileId> \
|
|
244
|
+
--accountId <accountId> \
|
|
245
|
+
--platformPostId <platformPostId> \
|
|
246
|
+
--name "Lead Magnet" \
|
|
247
|
+
--keywords "info,details,link" \
|
|
248
|
+
--matchMode contains \
|
|
249
|
+
--dmMessage "Here is the link you asked for" \
|
|
250
|
+
--commentReply "Check your DMs"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
zernio automations:update <automationId> --isActive false
|
|
255
|
+
zernio automations:logs <automationId> --status sent --pretty
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Omit `--keywords` only when the user explicitly wants every comment to trigger.
|
|
259
|
+
|
|
260
|
+
## Advanced API Use Cases
|
|
261
|
+
|
|
262
|
+
Use `api:*` for ads, webhooks, WhatsApp phone numbers/templates/flows/calling, workflows, usage, API keys, account groups, GMB, Discord, validation, and any endpoint not wrapped by curated commands.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
zernio api:catalog --tag Ads --search campaign --pretty
|
|
266
|
+
zernio api:describe createStandaloneAd --pretty
|
|
267
|
+
zernio api:call createStandaloneAd --body-file ./ad.json --idempotency-key ad_req_123 --pretty
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
zernio api:catalog --tag Webhooks --pretty
|
|
272
|
+
zernio api:describe createWebhookSettings --pretty
|
|
273
|
+
zernio api:call createWebhookSettings --body-file ./webhook.json --pretty
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
zernio api:catalog --tag Workflows --pretty
|
|
278
|
+
zernio api:call triggerWorkflow --path workflowId=<workflowId> --body-file ./workflow-trigger.json --pretty
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Dry Run Before Mutation
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
zernio api:call createPost --body-file ./post.json --dry-run --pretty
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Confirm method, URL, path params, and body presence before removing `--dry-run`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createClient } from '../client.js';
|
|
2
|
+
import { addAccountHealthDiagnostics, handlePostCreateError } from '../utils/posts-create-diagnostics.js';
|
|
3
|
+
import { applyTwitterPlatformSpecificData, buildMediaItems, buildTwitterPlatformSpecificData, PostsCreateValidationError, validateTwitterPlatformSpecificData, } from '../utils/posts-create-platform-data.js';
|
|
4
|
+
import { output, outputError } from '../utils/output.js';
|
|
5
|
+
export function registerPostCreateCommand(yargs) {
|
|
6
|
+
return yargs.command('posts:create', 'Create or schedule a post', (y) => y
|
|
7
|
+
.option('text', { type: 'string', describe: 'Post content text', demandOption: true })
|
|
8
|
+
.option('accounts', { type: 'string', describe: 'Comma-separated account IDs', demandOption: true })
|
|
9
|
+
.option('scheduledAt', { type: 'string', describe: 'ISO 8601 date to schedule (omit to publish now)' })
|
|
10
|
+
.option('draft', { type: 'boolean', describe: 'Save as draft', default: false })
|
|
11
|
+
.option('media', { type: 'string', describe: 'Comma-separated media URLs' })
|
|
12
|
+
.option('title', { type: 'string', describe: 'Post title (YouTube, Reddit, etc.)' })
|
|
13
|
+
.option('tags', { type: 'string', describe: 'Comma-separated tags' })
|
|
14
|
+
.option('hashtags', { type: 'string', describe: 'Comma-separated hashtags' })
|
|
15
|
+
.option('timezone', { type: 'string', describe: 'Timezone (e.g. America/New_York)' })
|
|
16
|
+
.option('quoteTweetId', { type: 'string', describe: 'X/Twitter tweet ID or status URL to quote' })
|
|
17
|
+
.option('replyToTweetId', { type: 'string', describe: 'X/Twitter tweet ID to reply to' })
|
|
18
|
+
.option('replySettings', {
|
|
19
|
+
type: 'string',
|
|
20
|
+
describe: 'X/Twitter reply settings: following, mentionedUsers, subscribers, verified',
|
|
21
|
+
})
|
|
22
|
+
.option('threadJson', { type: 'string', describe: 'X/Twitter threadItems as a JSON array' })
|
|
23
|
+
.option('threadFile', { type: 'string', describe: 'X/Twitter thread file, JSON array or --- separated text' })
|
|
24
|
+
.option('platformSpecificData', {
|
|
25
|
+
type: 'string',
|
|
26
|
+
describe: 'Advanced platformSpecificData JSON object for X/Twitter targets',
|
|
27
|
+
})
|
|
28
|
+
.option('debug-safe', {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
describe: 'Include non-secret post/account diagnostics when create fails',
|
|
31
|
+
default: false,
|
|
32
|
+
}), async (argv) => {
|
|
33
|
+
let platforms = [];
|
|
34
|
+
let selectedAccounts = [];
|
|
35
|
+
try {
|
|
36
|
+
const late = createClient();
|
|
37
|
+
const { data: accountsData } = await late.accounts.listAccounts();
|
|
38
|
+
const accountIds = argv.accounts.split(',').map((s) => s.trim()).filter(Boolean);
|
|
39
|
+
const allAccounts = accountsData?.accounts || [];
|
|
40
|
+
selectedAccounts = accountIds.map((id) => {
|
|
41
|
+
const account = allAccounts.find((a) => (a._id || a.id) === id);
|
|
42
|
+
if (!account) {
|
|
43
|
+
throw new PostsCreateValidationError(`Account ${id} not found. Run "zernio accounts:list" to see available accounts.`, 'ACCOUNT_NOT_FOUND', 404);
|
|
44
|
+
}
|
|
45
|
+
return account;
|
|
46
|
+
});
|
|
47
|
+
platforms = selectedAccounts.map((account, index) => ({
|
|
48
|
+
platform: String(account.platform),
|
|
49
|
+
accountId: accountIds[index],
|
|
50
|
+
}));
|
|
51
|
+
const mediaItems = buildMediaItems(argv.media);
|
|
52
|
+
const twitterData = buildTwitterPlatformSpecificData({
|
|
53
|
+
quoteTweetId: argv.quoteTweetId,
|
|
54
|
+
replyToTweetId: argv.replyToTweetId,
|
|
55
|
+
replySettings: argv.replySettings,
|
|
56
|
+
threadJson: argv.threadJson,
|
|
57
|
+
threadFile: argv.threadFile,
|
|
58
|
+
platformSpecificData: argv.platformSpecificData,
|
|
59
|
+
});
|
|
60
|
+
validateTwitterPlatformSpecificData(twitterData, platforms, mediaItems);
|
|
61
|
+
platforms = applyTwitterPlatformSpecificData(platforms, twitterData);
|
|
62
|
+
if (argv.debugSafe) {
|
|
63
|
+
selectedAccounts = await addAccountHealthDiagnostics(late, accountIds, selectedAccounts);
|
|
64
|
+
}
|
|
65
|
+
const body = buildCreatePostBody(argv, platforms, mediaItems);
|
|
66
|
+
const { data } = await late.posts.createPost({ body });
|
|
67
|
+
output(data, argv.pretty);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof PostsCreateValidationError) {
|
|
71
|
+
outputError(err.message, err.status, { code: err.code }, argv.pretty);
|
|
72
|
+
}
|
|
73
|
+
handlePostCreateError(err, {
|
|
74
|
+
debugSafe: argv.debugSafe,
|
|
75
|
+
pretty: argv.pretty,
|
|
76
|
+
platforms,
|
|
77
|
+
accounts: selectedAccounts,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function buildCreatePostBody(argv, platforms, mediaItems) {
|
|
83
|
+
const body = {
|
|
84
|
+
content: argv.text,
|
|
85
|
+
platforms,
|
|
86
|
+
};
|
|
87
|
+
if (mediaItems?.length)
|
|
88
|
+
body.mediaItems = mediaItems;
|
|
89
|
+
if (argv.title)
|
|
90
|
+
body.title = argv.title;
|
|
91
|
+
if (argv.timezone)
|
|
92
|
+
body.timezone = argv.timezone;
|
|
93
|
+
if (argv.tags)
|
|
94
|
+
body.tags = csv(argv.tags);
|
|
95
|
+
if (argv.hashtags)
|
|
96
|
+
body.hashtags = csv(argv.hashtags);
|
|
97
|
+
if (argv.draft) {
|
|
98
|
+
body.isDraft = true;
|
|
99
|
+
}
|
|
100
|
+
else if (argv.scheduledAt) {
|
|
101
|
+
body.scheduledFor = argv.scheduledAt;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
body.publishNow = true;
|
|
105
|
+
}
|
|
106
|
+
return body;
|
|
107
|
+
}
|
|
108
|
+
function csv(value) {
|
|
109
|
+
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
110
|
+
}
|
package/dist/commands/posts.js
CHANGED
|
@@ -1,70 +1,10 @@
|
|
|
1
1
|
import { createClient } from '../client.js';
|
|
2
|
-
import { output
|
|
2
|
+
import { output } from '../utils/output.js';
|
|
3
3
|
import { handleError } from '../utils/errors.js';
|
|
4
|
+
import { registerPostCreateCommand } from './posts-create.js';
|
|
4
5
|
/** Register post commands: posts:create, posts:list, posts:get, posts:delete, posts:retry */
|
|
5
6
|
export function registerPostCommands(yargs) {
|
|
6
|
-
return yargs
|
|
7
|
-
.command('posts:create', 'Create or schedule a post', (y) => y
|
|
8
|
-
.option('text', { type: 'string', describe: 'Post content text', demandOption: true })
|
|
9
|
-
.option('accounts', { type: 'string', describe: 'Comma-separated account IDs', demandOption: true })
|
|
10
|
-
.option('scheduledAt', { type: 'string', describe: 'ISO 8601 date to schedule (omit to publish now)' })
|
|
11
|
-
.option('draft', { type: 'boolean', describe: 'Save as draft', default: false })
|
|
12
|
-
.option('media', { type: 'string', describe: 'Comma-separated media URLs' })
|
|
13
|
-
.option('title', { type: 'string', describe: 'Post title (YouTube, Reddit, etc.)' })
|
|
14
|
-
.option('tags', { type: 'string', describe: 'Comma-separated tags' })
|
|
15
|
-
.option('hashtags', { type: 'string', describe: 'Comma-separated hashtags' })
|
|
16
|
-
.option('timezone', { type: 'string', describe: 'Timezone (e.g. America/New_York)' }), async (argv) => {
|
|
17
|
-
try {
|
|
18
|
-
const late = createClient();
|
|
19
|
-
// Look up accounts to resolve platform types
|
|
20
|
-
const { data: accountsData } = await late.accounts.listAccounts();
|
|
21
|
-
const allAccounts = accountsData?.accounts || [];
|
|
22
|
-
const accountIds = argv.accounts.split(',').map((s) => s.trim()).filter(Boolean);
|
|
23
|
-
const platforms = accountIds.map((id) => {
|
|
24
|
-
const account = allAccounts.find((a) => (a._id || a.id) === id);
|
|
25
|
-
if (!account) {
|
|
26
|
-
outputError(`Account ${id} not found. Run "late accounts:list" to see available accounts.`, 404);
|
|
27
|
-
}
|
|
28
|
-
return { platform: account.platform, accountId: id };
|
|
29
|
-
});
|
|
30
|
-
// Build media items
|
|
31
|
-
const mediaItems = argv.media
|
|
32
|
-
? argv.media.split(',').map((url) => {
|
|
33
|
-
const trimmed = url.trim();
|
|
34
|
-
const isVideo = /\.(mp4|mov|avi|webm|m4v)$/i.test(trimmed);
|
|
35
|
-
return { type: (isVideo ? 'video' : 'image'), url: trimmed };
|
|
36
|
-
})
|
|
37
|
-
: undefined;
|
|
38
|
-
const body = {
|
|
39
|
-
content: argv.text,
|
|
40
|
-
platforms,
|
|
41
|
-
};
|
|
42
|
-
if (mediaItems?.length)
|
|
43
|
-
body.mediaItems = mediaItems;
|
|
44
|
-
if (argv.title)
|
|
45
|
-
body.title = argv.title;
|
|
46
|
-
if (argv.timezone)
|
|
47
|
-
body.timezone = argv.timezone;
|
|
48
|
-
if (argv.tags)
|
|
49
|
-
body.tags = argv.tags.split(',').map((s) => s.trim());
|
|
50
|
-
if (argv.hashtags)
|
|
51
|
-
body.hashtags = argv.hashtags.split(',').map((s) => s.trim());
|
|
52
|
-
if (argv.draft) {
|
|
53
|
-
body.isDraft = true;
|
|
54
|
-
}
|
|
55
|
-
else if (argv.scheduledAt) {
|
|
56
|
-
body.scheduledFor = argv.scheduledAt;
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
body.publishNow = true;
|
|
60
|
-
}
|
|
61
|
-
const { data } = await late.posts.createPost({ body });
|
|
62
|
-
output(data, argv.pretty);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
handleError(err);
|
|
66
|
-
}
|
|
67
|
-
})
|
|
7
|
+
return registerPostCreateCommand(yargs)
|
|
68
8
|
.command('posts:list', 'List posts', (y) => y
|
|
69
9
|
.option('profileId', { type: 'string', describe: 'Filter by profile ID' })
|
|
70
10
|
.option('status', { type: 'string', describe: 'Filter by status (scheduled, published, failed, draft)' })
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PlatformTarget } from './posts-create-platform-data.js';
|
|
2
|
+
type AccountRecord = Record<string, unknown>;
|
|
3
|
+
type PostCreateErrorContext = {
|
|
4
|
+
debugSafe: boolean;
|
|
5
|
+
pretty: boolean;
|
|
6
|
+
platforms: PlatformTarget[];
|
|
7
|
+
accounts: AccountRecord[];
|
|
8
|
+
};
|
|
9
|
+
export declare function handlePostCreateError(err: unknown, context: PostCreateErrorContext): never;
|
|
10
|
+
export declare function buildPostCreateDiagnostic(context: PostCreateErrorContext, status?: number): Record<string, unknown>;
|
|
11
|
+
export declare function addAccountHealthDiagnostics(late: {
|
|
12
|
+
accounts?: {
|
|
13
|
+
getAccountHealth?: (args: {
|
|
14
|
+
path: {
|
|
15
|
+
accountId: string;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
data: unknown;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
}, accountIds: string[], accounts: AccountRecord[]): Promise<AccountRecord[]>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { LateApiError } from '@zernio/node';
|
|
2
|
+
import { handleError } from './errors.js';
|
|
3
|
+
import { outputError } from './output.js';
|
|
4
|
+
const SAFE_ACCOUNT_FIELDS = [
|
|
5
|
+
'profileId',
|
|
6
|
+
'profileName',
|
|
7
|
+
'platform',
|
|
8
|
+
'username',
|
|
9
|
+
'displayName',
|
|
10
|
+
'status',
|
|
11
|
+
'canPost',
|
|
12
|
+
'tokenValid',
|
|
13
|
+
'needsReconnect',
|
|
14
|
+
'issues',
|
|
15
|
+
];
|
|
16
|
+
export function handlePostCreateError(err, context) {
|
|
17
|
+
if (!context.debugSafe)
|
|
18
|
+
handleError(err);
|
|
19
|
+
const status = err instanceof LateApiError ? err.statusCode : undefined;
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
outputError(message, status, {
|
|
22
|
+
code: status === 401 ? 'POST_CREATE_UNAUTHORIZED' : 'POST_CREATE_FAILED',
|
|
23
|
+
diagnostic: buildPostCreateDiagnostic(context, status),
|
|
24
|
+
}, context.pretty);
|
|
25
|
+
}
|
|
26
|
+
export function buildPostCreateDiagnostic(context, status) {
|
|
27
|
+
return {
|
|
28
|
+
command: 'posts:create',
|
|
29
|
+
resolvedPlatforms: context.platforms,
|
|
30
|
+
targetAccounts: context.platforms.map((target) => summarizeAccount(target, context.accounts)),
|
|
31
|
+
suggestions: suggestionsForStatus(status),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function addAccountHealthDiagnostics(late, accountIds, accounts) {
|
|
35
|
+
if (!late.accounts?.getAccountHealth)
|
|
36
|
+
return accounts;
|
|
37
|
+
const healthEntries = await Promise.all(accountIds.map(async (id) => {
|
|
38
|
+
try {
|
|
39
|
+
const { data } = await late.accounts.getAccountHealth({ path: { accountId: id } });
|
|
40
|
+
return [id, data];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [id, undefined];
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
const healthById = new Map(healthEntries.filter((entry) => isRecord(entry[1])));
|
|
47
|
+
return accounts.map((account) => {
|
|
48
|
+
const id = accountId(account);
|
|
49
|
+
return id && healthById.has(id) ? { ...account, ...healthById.get(id) } : account;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function summarizeAccount(target, accounts) {
|
|
53
|
+
const account = accounts.find((candidate) => accountId(candidate) === target.accountId) || {};
|
|
54
|
+
const summary = {
|
|
55
|
+
accountId: target.accountId,
|
|
56
|
+
platform: target.platform,
|
|
57
|
+
};
|
|
58
|
+
for (const field of SAFE_ACCOUNT_FIELDS) {
|
|
59
|
+
if (account[field] !== undefined && field !== 'platform')
|
|
60
|
+
summary[field] = account[field];
|
|
61
|
+
}
|
|
62
|
+
return summary;
|
|
63
|
+
}
|
|
64
|
+
function accountId(account) {
|
|
65
|
+
const id = account.id || account._id || account.accountId;
|
|
66
|
+
return id === undefined ? undefined : String(id);
|
|
67
|
+
}
|
|
68
|
+
function isRecord(value) {
|
|
69
|
+
return typeof value === 'object' && value !== null;
|
|
70
|
+
}
|
|
71
|
+
function suggestionsForStatus(status) {
|
|
72
|
+
if (status === 401) {
|
|
73
|
+
return [
|
|
74
|
+
'Run zernio auth:check --pretty to verify the API key.',
|
|
75
|
+
'Run zernio accounts:health --pretty and compare the target account status.',
|
|
76
|
+
'If health is valid but create still returns 401, reconnect the account or report a backend authorization mismatch.',
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
'Run zernio doctor --connection --pretty to verify API connectivity.',
|
|
81
|
+
'Run the same payload with zernio api:call createPost --dry-run to inspect request shape.',
|
|
82
|
+
];
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { PostsCreateValidationError } from './posts-create-validation-error.js';
|
|
2
|
+
export type MediaItem = {
|
|
3
|
+
type: 'image' | 'video';
|
|
4
|
+
url: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
};
|
|
7
|
+
export type PlatformTarget = {
|
|
8
|
+
platform: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
platformSpecificData?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
export type TwitterPlatformSpecificDataResult = {
|
|
13
|
+
hasData: boolean;
|
|
14
|
+
data?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
type TwitterPlatformOptions = {
|
|
17
|
+
quoteTweetId?: unknown;
|
|
18
|
+
replyToTweetId?: unknown;
|
|
19
|
+
replySettings?: unknown;
|
|
20
|
+
threadJson?: unknown;
|
|
21
|
+
threadFile?: unknown;
|
|
22
|
+
platformSpecificData?: unknown;
|
|
23
|
+
};
|
|
24
|
+
export declare function buildMediaItems(media?: unknown): MediaItem[] | undefined;
|
|
25
|
+
export declare function buildTwitterPlatformSpecificData(options: TwitterPlatformOptions): TwitterPlatformSpecificDataResult;
|
|
26
|
+
export declare function validateTwitterPlatformSpecificData(result: TwitterPlatformSpecificDataResult, platforms: PlatformTarget[], mediaItems?: MediaItem[]): void;
|
|
27
|
+
export declare function applyTwitterPlatformSpecificData(platforms: PlatformTarget[], result: TwitterPlatformSpecificDataResult): PlatformTarget[];
|