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.
@@ -1,58 +1,287 @@
1
1
  # Zernio Workflows
2
2
 
3
- ## Auth
3
+ Use these flows as recipes. Replace IDs with values from discovery commands.
4
4
 
5
- Use browser login for local human sessions:
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
- Use env for agents and CI:
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
- ## Post With Media
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
- Media upload is a two-step server workflow: presign, upload direct to returned URL, then use returned public URL in post payload.
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
- Use:
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
- Optional `queueId` targets a specific queue. Do not use `next-slot` as `scheduledFor`; it is preview-only.
157
+ ## Inbox, Comments, Reviews
41
158
 
42
- ## Full API Calls
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 api:catalog --tag Posts --pretty
46
- zernio api:describe createPost --pretty
47
- zernio api:call createPost --body-file ./post.json --pretty
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
- Use `--dry-run` before mutating calls when building automation.
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
- ## Common Checks
176
+ ## Contacts and CRM
53
177
 
54
178
  ```bash
55
- zernio platforms:list --pretty
56
- zernio accounts:health --pretty
57
- zernio api:catalog --search webhook --pretty
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,2 @@
1
+ import type { Argv } from 'yargs';
2
+ export declare function registerPostCreateCommand(yargs: Argv): Argv;
@@ -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
+ }
@@ -1,70 +1,10 @@
1
1
  import { createClient } from '../client.js';
2
- import { output, outputError } from '../utils/output.js';
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[];