zernio-cli 0.4.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.
Files changed (63) hide show
  1. package/README.md +145 -0
  2. package/SKILL.md +98 -0
  3. package/claude/plugin.json +23 -0
  4. package/claude/skills/zernio/SKILL.md +83 -0
  5. package/claude/skills/zernio/references/zernio-api-surface.md +48 -0
  6. package/claude/skills/zernio/references/zernio-best-practices.md +33 -0
  7. package/claude/skills/zernio/references/zernio-workflows.md +58 -0
  8. package/dist/client.d.ts +6 -0
  9. package/dist/client.js +14 -0
  10. package/dist/commands/accounts.d.ts +3 -0
  11. package/dist/commands/accounts.js +53 -0
  12. package/dist/commands/analytics.d.ts +3 -0
  13. package/dist/commands/analytics.js +85 -0
  14. package/dist/commands/api.d.ts +2 -0
  15. package/dist/commands/api.js +108 -0
  16. package/dist/commands/auth.d.ts +3 -0
  17. package/dist/commands/auth.js +138 -0
  18. package/dist/commands/automations.d.ts +5 -0
  19. package/dist/commands/automations.js +139 -0
  20. package/dist/commands/broadcasts.d.ts +5 -0
  21. package/dist/commands/broadcasts.js +184 -0
  22. package/dist/commands/contacts.d.ts +6 -0
  23. package/dist/commands/contacts.js +198 -0
  24. package/dist/commands/doctor.d.ts +2 -0
  25. package/dist/commands/doctor.js +51 -0
  26. package/dist/commands/inbox.d.ts +6 -0
  27. package/dist/commands/inbox.js +222 -0
  28. package/dist/commands/media.d.ts +3 -0
  29. package/dist/commands/media.js +76 -0
  30. package/dist/commands/platforms.d.ts +2 -0
  31. package/dist/commands/platforms.js +27 -0
  32. package/dist/commands/posts.d.ts +3 -0
  33. package/dist/commands/posts.js +129 -0
  34. package/dist/commands/profiles.d.ts +3 -0
  35. package/dist/commands/profiles.js +82 -0
  36. package/dist/commands/sequences.d.ts +5 -0
  37. package/dist/commands/sequences.js +178 -0
  38. package/dist/generated/openapi-catalog.d.ts +8961 -0
  39. package/dist/generated/openapi-catalog.js +3 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +61 -0
  42. package/dist/utils/api-request.d.ts +31 -0
  43. package/dist/utils/api-request.js +115 -0
  44. package/dist/utils/argument-parsing.d.ts +3 -0
  45. package/dist/utils/argument-parsing.js +27 -0
  46. package/dist/utils/config.d.ts +27 -0
  47. package/dist/utils/config.js +111 -0
  48. package/dist/utils/errors.d.ts +5 -0
  49. package/dist/utils/errors.js +12 -0
  50. package/dist/utils/openapi-catalog.d.ts +16 -0
  51. package/dist/utils/openapi-catalog.js +58 -0
  52. package/dist/utils/output.d.ts +9 -0
  53. package/dist/utils/output.js +24 -0
  54. package/docs/architecture.md +37 -0
  55. package/docs/cli.md +99 -0
  56. package/docs/code-standards.md +11 -0
  57. package/docs/contributing.md +34 -0
  58. package/docs/development-roadmap.md +32 -0
  59. package/docs/openapi/zernio-api-openapi.yaml +30967 -0
  60. package/docs/project-changelog.md +15 -0
  61. package/docs/project-overview-pdr.md +25 -0
  62. package/docs/system-architecture.md +28 -0
  63. package/package.json +82 -0
@@ -0,0 +1,198 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /**
5
+ * Register contact commands: contacts:list, contacts:create, contacts:get, contacts:update,
6
+ * contacts:delete, contacts:channels, contacts:set-field, contacts:clear-field, contacts:bulk-create.
7
+ */
8
+ export function registerContactCommands(yargs) {
9
+ return yargs
10
+ .command('contacts:list', 'List contacts', (y) => y
11
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
12
+ .option('search', { type: 'string', describe: 'Search by name, email, or phone' })
13
+ .option('tag', { type: 'string', describe: 'Filter by tag' })
14
+ .option('platform', { type: 'string', describe: 'Filter by platform' })
15
+ .option('isSubscribed', { type: 'string', describe: 'Filter by subscription status (true, false)' })
16
+ .option('limit', { type: 'number', describe: 'Max results', default: 50 })
17
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
18
+ try {
19
+ const late = createClient();
20
+ const query = {
21
+ limit: argv.limit,
22
+ skip: argv.skip,
23
+ };
24
+ if (argv.profileId)
25
+ query.profileId = argv.profileId;
26
+ if (argv.search)
27
+ query.search = argv.search;
28
+ if (argv.tag)
29
+ query.tag = argv.tag;
30
+ if (argv.platform)
31
+ query.platform = argv.platform;
32
+ if (argv.isSubscribed)
33
+ query.isSubscribed = argv.isSubscribed;
34
+ const { data } = await late.contacts.listContacts({ query });
35
+ output(data, argv.pretty);
36
+ }
37
+ catch (err) {
38
+ handleError(err);
39
+ }
40
+ })
41
+ .command('contacts:create', 'Create a contact', (y) => y
42
+ .option('profileId', { type: 'string', describe: 'Profile ID', demandOption: true })
43
+ .option('name', { type: 'string', describe: 'Contact name', demandOption: true })
44
+ .option('email', { type: 'string', describe: 'Email address' })
45
+ .option('company', { type: 'string', describe: 'Company name' })
46
+ .option('tags', { type: 'string', describe: 'Comma-separated tags' })
47
+ .option('accountId', { type: 'string', describe: 'Account ID (creates a channel if provided with platform + platformIdentifier)' })
48
+ .option('platform', { type: 'string', describe: 'Platform for initial channel' })
49
+ .option('platformIdentifier', { type: 'string', describe: 'Platform user ID for initial channel' }), async (argv) => {
50
+ try {
51
+ const late = createClient();
52
+ const body = {
53
+ profileId: argv.profileId,
54
+ name: argv.name,
55
+ };
56
+ if (argv.email)
57
+ body.email = argv.email;
58
+ if (argv.company)
59
+ body.company = argv.company;
60
+ if (argv.tags)
61
+ body.tags = argv.tags.split(',').map((s) => s.trim());
62
+ if (argv.accountId)
63
+ body.accountId = argv.accountId;
64
+ if (argv.platform)
65
+ body.platform = argv.platform;
66
+ if (argv.platformIdentifier)
67
+ body.platformIdentifier = argv.platformIdentifier;
68
+ const { data } = await late.contacts.createContact({ body: body });
69
+ output(data, argv.pretty);
70
+ }
71
+ catch (err) {
72
+ handleError(err);
73
+ }
74
+ })
75
+ .command('contacts:get <id>', 'Get contact details', (y) => y.positional('id', { type: 'string', describe: 'Contact ID', demandOption: true }), async (argv) => {
76
+ try {
77
+ const late = createClient();
78
+ const { data } = await late.contacts.getContact({ path: { contactId: argv.id } });
79
+ output(data, argv.pretty);
80
+ }
81
+ catch (err) {
82
+ handleError(err);
83
+ }
84
+ })
85
+ .command('contacts:update <id>', 'Update a contact', (y) => y
86
+ .positional('id', { type: 'string', describe: 'Contact ID', demandOption: true })
87
+ .option('name', { type: 'string', describe: 'Contact name' })
88
+ .option('email', { type: 'string', describe: 'Email address' })
89
+ .option('company', { type: 'string', describe: 'Company name' })
90
+ .option('tags', { type: 'string', describe: 'Comma-separated tags (replaces existing)' })
91
+ .option('isSubscribed', { type: 'boolean', describe: 'Subscription status' })
92
+ .option('isBlocked', { type: 'boolean', describe: 'Block status' })
93
+ .option('notes', { type: 'string', describe: 'Contact notes' }), async (argv) => {
94
+ try {
95
+ const late = createClient();
96
+ const body = {};
97
+ if (argv.name)
98
+ body.name = argv.name;
99
+ if (argv.email)
100
+ body.email = argv.email;
101
+ if (argv.company)
102
+ body.company = argv.company;
103
+ if (argv.tags)
104
+ body.tags = argv.tags.split(',').map((s) => s.trim());
105
+ if (argv.isSubscribed !== undefined)
106
+ body.isSubscribed = argv.isSubscribed;
107
+ if (argv.isBlocked !== undefined)
108
+ body.isBlocked = argv.isBlocked;
109
+ if (argv.notes)
110
+ body.notes = argv.notes;
111
+ const { data } = await late.contacts.updateContact({
112
+ path: { contactId: argv.id },
113
+ body: body,
114
+ });
115
+ output(data, argv.pretty);
116
+ }
117
+ catch (err) {
118
+ handleError(err);
119
+ }
120
+ })
121
+ .command('contacts:delete <id>', 'Delete a contact', (y) => y.positional('id', { type: 'string', describe: 'Contact ID', demandOption: true }), async (argv) => {
122
+ try {
123
+ const late = createClient();
124
+ const { data } = await late.contacts.deleteContact({ path: { contactId: argv.id } });
125
+ output(data, argv.pretty);
126
+ }
127
+ catch (err) {
128
+ handleError(err);
129
+ }
130
+ })
131
+ .command('contacts:channels <id>', 'List channels for a contact', (y) => y.positional('id', { type: 'string', describe: 'Contact ID', demandOption: true }), async (argv) => {
132
+ try {
133
+ const late = createClient();
134
+ const { data } = await late.contacts.getContactChannels({ path: { contactId: argv.id } });
135
+ output(data, argv.pretty);
136
+ }
137
+ catch (err) {
138
+ handleError(err);
139
+ }
140
+ })
141
+ .command('contacts:set-field <id> <slug>', 'Set a custom field value on a contact', (y) => y
142
+ .positional('id', { type: 'string', describe: 'Contact ID', demandOption: true })
143
+ .positional('slug', { type: 'string', describe: 'Field slug', demandOption: true })
144
+ .option('value', { type: 'string', describe: 'Field value', demandOption: true }), async (argv) => {
145
+ try {
146
+ const late = createClient();
147
+ const { data } = await late.customfields.setContactFieldValue({
148
+ path: { contactId: argv.id, slug: argv.slug },
149
+ body: { value: argv.value },
150
+ });
151
+ output(data, argv.pretty);
152
+ }
153
+ catch (err) {
154
+ handleError(err);
155
+ }
156
+ })
157
+ .command('contacts:clear-field <id> <slug>', 'Clear a custom field value on a contact', (y) => y
158
+ .positional('id', { type: 'string', describe: 'Contact ID', demandOption: true })
159
+ .positional('slug', { type: 'string', describe: 'Field slug', demandOption: true }), async (argv) => {
160
+ try {
161
+ const late = createClient();
162
+ const { data } = await late.customfields.clearContactFieldValue({
163
+ path: { contactId: argv.id, slug: argv.slug },
164
+ });
165
+ output(data, argv.pretty);
166
+ }
167
+ catch (err) {
168
+ handleError(err);
169
+ }
170
+ })
171
+ .command('contacts:bulk-create', 'Bulk create up to 1000 contacts from a JSON file', (y) => y
172
+ .option('profileId', { type: 'string', describe: 'Profile ID', demandOption: true })
173
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
174
+ .option('platform', { type: 'string', describe: 'Platform', demandOption: true })
175
+ .option('file', { type: 'string', describe: 'Path to JSON file with contacts array', demandOption: true }), async (argv) => {
176
+ try {
177
+ const { readFileSync } = await import('fs');
178
+ const raw = readFileSync(argv.file, 'utf-8');
179
+ const contacts = JSON.parse(raw);
180
+ if (!Array.isArray(contacts)) {
181
+ throw new Error('JSON file must contain an array of contact objects');
182
+ }
183
+ const late = createClient();
184
+ const { data } = await late.contacts.bulkCreateContacts({
185
+ body: {
186
+ profileId: argv.profileId,
187
+ accountId: argv.accountId,
188
+ platform: argv.platform,
189
+ contacts,
190
+ },
191
+ });
192
+ output(data, argv.pretty);
193
+ }
194
+ catch (err) {
195
+ handleError(err);
196
+ }
197
+ });
198
+ }
@@ -0,0 +1,2 @@
1
+ import type { Argv } from 'yargs';
2
+ export declare function registerDoctorCommand(yargs: Argv): Argv;
@@ -0,0 +1,51 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { version as nodeVersion } from 'node:process';
3
+ import { resolveConfig } from '../utils/config.js';
4
+ import { runApiRequest } from '../utils/api-request.js';
5
+ import { output } from '../utils/output.js';
6
+ import { handleError } from '../utils/errors.js';
7
+ const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
8
+ export function registerDoctorCommand(yargs) {
9
+ return yargs.command('doctor', 'Show redacted CLI configuration and optional connection status', (y) => y.option('connection', {
10
+ type: 'boolean',
11
+ describe: 'Call the user endpoint to verify API connectivity',
12
+ default: false,
13
+ }), async (argv) => {
14
+ try {
15
+ const resolved = resolveConfig();
16
+ const report = {
17
+ cli: {
18
+ name: packageJson.name,
19
+ version: packageJson.version,
20
+ node: nodeVersion,
21
+ },
22
+ config: {
23
+ apiKey: {
24
+ resolved: Boolean(resolved.config.apiKey),
25
+ source: resolved.sources.apiKey,
26
+ },
27
+ baseUrl: {
28
+ value: resolved.config.baseUrl || 'https://zernio.com/api',
29
+ source: resolved.sources.baseUrl || 'default',
30
+ },
31
+ },
32
+ };
33
+ if (argv.connection) {
34
+ const result = await runApiRequest({ method: 'GET', path: '/v1/users' });
35
+ report.connection = {
36
+ ok: result.ok,
37
+ status: result.status,
38
+ rateLimit: result.rateLimit,
39
+ };
40
+ output(report, argv.pretty);
41
+ if (!result.ok)
42
+ process.exit(1);
43
+ return;
44
+ }
45
+ output(report, argv.pretty);
46
+ }
47
+ catch (err) {
48
+ handleError(err);
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,6 @@
1
+ import type { Argv } from 'yargs';
2
+ /**
3
+ * Register inbox commands: conversations, messages, comments, and reviews.
4
+ * Uses the SDK's messages, comments, and reviews namespaces.
5
+ */
6
+ export declare function registerInboxCommands(yargs: Argv): Argv;
@@ -0,0 +1,222 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /**
5
+ * Register inbox commands: conversations, messages, comments, and reviews.
6
+ * Uses the SDK's messages, comments, and reviews namespaces.
7
+ */
8
+ export function registerInboxCommands(yargs) {
9
+ return yargs
10
+ .command('inbox:conversations', 'List DM conversations', (y) => y
11
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
12
+ .option('accountId', { type: 'string', describe: 'Filter by account ID' })
13
+ .option('platform', { type: 'string', describe: 'Filter by platform (facebook, instagram, twitter, bluesky, reddit, telegram)' })
14
+ .option('status', { type: 'string', describe: 'Filter by status (active, archived)' })
15
+ .option('sortOrder', { type: 'string', describe: 'Sort order (asc, desc)', default: 'desc' })
16
+ .option('limit', { type: 'number', describe: 'Max results', default: 20 })
17
+ .option('cursor', { type: 'string', describe: 'Pagination cursor' }), async (argv) => {
18
+ try {
19
+ const late = createClient();
20
+ const query = {};
21
+ if (argv.profileId)
22
+ query.profileId = argv.profileId;
23
+ if (argv.accountId)
24
+ query.accountId = argv.accountId;
25
+ if (argv.platform)
26
+ query.platform = argv.platform;
27
+ if (argv.status)
28
+ query.status = argv.status;
29
+ if (argv.sortOrder)
30
+ query.sortOrder = argv.sortOrder;
31
+ if (argv.limit)
32
+ query.limit = argv.limit;
33
+ if (argv.cursor)
34
+ query.cursor = argv.cursor;
35
+ const { data } = await late.messages.listInboxConversations({ query });
36
+ output(data, argv.pretty);
37
+ }
38
+ catch (err) {
39
+ handleError(err);
40
+ }
41
+ })
42
+ .command('inbox:conversation <id>', 'Get conversation details', (y) => y
43
+ .positional('id', { type: 'string', describe: 'Conversation ID', demandOption: true })
44
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true }), async (argv) => {
45
+ try {
46
+ const late = createClient();
47
+ const { data } = await late.messages.getInboxConversation({
48
+ path: { conversationId: argv.id },
49
+ query: { accountId: argv.accountId },
50
+ });
51
+ output(data, argv.pretty);
52
+ }
53
+ catch (err) {
54
+ handleError(err);
55
+ }
56
+ })
57
+ .command('inbox:messages <conversationId>', 'Get messages in a conversation', (y) => y
58
+ .positional('conversationId', { type: 'string', describe: 'Conversation ID', demandOption: true })
59
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true }), async (argv) => {
60
+ try {
61
+ const late = createClient();
62
+ const { data } = await late.messages.getInboxConversationMessages({
63
+ path: { conversationId: argv.conversationId },
64
+ query: { accountId: argv.accountId },
65
+ });
66
+ output(data, argv.pretty);
67
+ }
68
+ catch (err) {
69
+ handleError(err);
70
+ }
71
+ })
72
+ .command('inbox:send <conversationId>', 'Send a DM in a conversation', (y) => y
73
+ .positional('conversationId', { type: 'string', describe: 'Conversation ID', demandOption: true })
74
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
75
+ .option('message', { type: 'string', describe: 'Message text', demandOption: true })
76
+ .option('mediaUrl', { type: 'string', describe: 'Media attachment URL' }), async (argv) => {
77
+ try {
78
+ const late = createClient();
79
+ const body = {
80
+ accountId: argv.accountId,
81
+ message: argv.message,
82
+ };
83
+ if (argv.mediaUrl)
84
+ body.mediaUrl = argv.mediaUrl;
85
+ const { data } = await late.messages.sendInboxMessage({
86
+ path: { conversationId: argv.conversationId },
87
+ body: body,
88
+ });
89
+ output(data, argv.pretty);
90
+ }
91
+ catch (err) {
92
+ handleError(err);
93
+ }
94
+ })
95
+ .command('inbox:comments', 'List post comments across accounts', (y) => y
96
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
97
+ .option('accountId', { type: 'string', describe: 'Filter by account ID' })
98
+ .option('platform', { type: 'string', describe: 'Filter by platform' })
99
+ .option('since', { type: 'string', describe: 'Posts created after this date (ISO 8601)' })
100
+ .option('sortBy', { type: 'string', describe: 'Sort field (date, comments)', default: 'date' })
101
+ .option('limit', { type: 'number', describe: 'Max results', default: 20 })
102
+ .option('cursor', { type: 'string', describe: 'Pagination cursor' }), async (argv) => {
103
+ try {
104
+ const late = createClient();
105
+ const query = {};
106
+ if (argv.profileId)
107
+ query.profileId = argv.profileId;
108
+ if (argv.accountId)
109
+ query.accountId = argv.accountId;
110
+ if (argv.platform)
111
+ query.platform = argv.platform;
112
+ if (argv.since)
113
+ query.since = argv.since;
114
+ if (argv.sortBy)
115
+ query.sortBy = argv.sortBy;
116
+ if (argv.limit)
117
+ query.limit = argv.limit;
118
+ if (argv.cursor)
119
+ query.cursor = argv.cursor;
120
+ const { data } = await late.comments.listInboxComments({ query });
121
+ output(data, argv.pretty);
122
+ }
123
+ catch (err) {
124
+ handleError(err);
125
+ }
126
+ })
127
+ .command('inbox:post-comments <postId>', 'Get comments on a specific post', (y) => y
128
+ .positional('postId', { type: 'string', describe: 'Post ID', demandOption: true })
129
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true }), async (argv) => {
130
+ try {
131
+ const late = createClient();
132
+ const { data } = await late.comments.getInboxPostComments({
133
+ path: { postId: argv.postId },
134
+ query: { accountId: argv.accountId },
135
+ });
136
+ output(data, argv.pretty);
137
+ }
138
+ catch (err) {
139
+ handleError(err);
140
+ }
141
+ })
142
+ .command('inbox:reply <postId>', 'Reply to a comment on a post', (y) => y
143
+ .positional('postId', { type: 'string', describe: 'Post ID', demandOption: true })
144
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
145
+ .option('message', { type: 'string', describe: 'Reply text', demandOption: true })
146
+ .option('commentId', { type: 'string', describe: 'Reply to specific comment ID (optional)' }), async (argv) => {
147
+ try {
148
+ const late = createClient();
149
+ const body = {
150
+ accountId: argv.accountId,
151
+ message: argv.message,
152
+ };
153
+ if (argv.commentId)
154
+ body.commentId = argv.commentId;
155
+ const { data } = await late.comments.replyToInboxPost({
156
+ path: { postId: argv.postId },
157
+ body: body,
158
+ });
159
+ output(data, argv.pretty);
160
+ }
161
+ catch (err) {
162
+ handleError(err);
163
+ }
164
+ })
165
+ .command('inbox:reviews', 'List reviews across accounts', (y) => y
166
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
167
+ .option('accountId', { type: 'string', describe: 'Filter by account ID' })
168
+ .option('platform', { type: 'string', describe: 'Filter by platform (facebook, googlebusiness)' })
169
+ .option('hasReply', { type: 'boolean', describe: 'Filter by reply status' })
170
+ .option('minRating', { type: 'number', describe: 'Minimum rating' })
171
+ .option('maxRating', { type: 'number', describe: 'Maximum rating' })
172
+ .option('sortBy', { type: 'string', describe: 'Sort field (date, rating)', default: 'date' })
173
+ .option('sortOrder', { type: 'string', describe: 'Sort order (asc, desc)', default: 'desc' })
174
+ .option('limit', { type: 'number', describe: 'Max results', default: 20 })
175
+ .option('cursor', { type: 'string', describe: 'Pagination cursor' }), async (argv) => {
176
+ try {
177
+ const late = createClient();
178
+ const query = {};
179
+ if (argv.profileId)
180
+ query.profileId = argv.profileId;
181
+ if (argv.accountId)
182
+ query.accountId = argv.accountId;
183
+ if (argv.platform)
184
+ query.platform = argv.platform;
185
+ if (argv.hasReply !== undefined)
186
+ query.hasReply = argv.hasReply;
187
+ if (argv.minRating !== undefined)
188
+ query.minRating = argv.minRating;
189
+ if (argv.maxRating !== undefined)
190
+ query.maxRating = argv.maxRating;
191
+ if (argv.sortBy)
192
+ query.sortBy = argv.sortBy;
193
+ if (argv.sortOrder)
194
+ query.sortOrder = argv.sortOrder;
195
+ if (argv.limit)
196
+ query.limit = argv.limit;
197
+ if (argv.cursor)
198
+ query.cursor = argv.cursor;
199
+ const { data } = await late.reviews.listInboxReviews({ query });
200
+ output(data, argv.pretty);
201
+ }
202
+ catch (err) {
203
+ handleError(err);
204
+ }
205
+ })
206
+ .command('inbox:review-reply <reviewId>', 'Reply to a review', (y) => y
207
+ .positional('reviewId', { type: 'string', describe: 'Review ID', demandOption: true })
208
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
209
+ .option('message', { type: 'string', describe: 'Reply text', demandOption: true }), async (argv) => {
210
+ try {
211
+ const late = createClient();
212
+ const { data } = await late.reviews.replyToInboxReview({
213
+ path: { reviewId: argv.reviewId },
214
+ body: { accountId: argv.accountId, message: argv.message },
215
+ });
216
+ output(data, argv.pretty);
217
+ }
218
+ catch (err) {
219
+ handleError(err);
220
+ }
221
+ });
222
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ /** Register media commands: media:upload */
3
+ export declare function registerMediaCommands(yargs: Argv): Argv;
@@ -0,0 +1,76 @@
1
+ import { readFileSync, statSync } from 'fs';
2
+ import { basename } from 'path';
3
+ import { createClient } from '../client.js';
4
+ import { output, outputError } from '../utils/output.js';
5
+ import { handleError } from '../utils/errors.js';
6
+ /** Minimal MIME lookup for media types Late supports. */
7
+ const MIME_MAP = {
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.png': 'image/png',
11
+ '.webp': 'image/webp',
12
+ '.gif': 'image/gif',
13
+ '.mp4': 'video/mp4',
14
+ '.mov': 'video/quicktime',
15
+ '.avi': 'video/x-msvideo',
16
+ '.webm': 'video/webm',
17
+ '.m4v': 'video/x-m4v',
18
+ '.mpeg': 'video/mpeg',
19
+ '.pdf': 'application/pdf',
20
+ };
21
+ function getMimeType(filename) {
22
+ const ext = filename.slice(filename.lastIndexOf('.')).toLowerCase();
23
+ return MIME_MAP[ext] || 'application/octet-stream';
24
+ }
25
+ /** Register media commands: media:upload */
26
+ export function registerMediaCommands(yargs) {
27
+ return yargs.command('media:upload <file>', 'Upload a media file (returns URL for use in posts)', (y) => y.positional('file', { type: 'string', describe: 'Path to media file', demandOption: true }), async (argv) => {
28
+ try {
29
+ const filePath = argv.file;
30
+ // Validate file exists
31
+ let stat;
32
+ try {
33
+ stat = statSync(filePath);
34
+ }
35
+ catch {
36
+ outputError(`File not found: ${filePath}`);
37
+ }
38
+ if (!stat.isFile()) {
39
+ outputError(`Not a file: ${filePath}`);
40
+ }
41
+ const fileName = basename(filePath);
42
+ const contentType = getMimeType(fileName);
43
+ const fileBuffer = readFileSync(filePath);
44
+ // Get presigned URL via SDK
45
+ const late = createClient();
46
+ const { data: presign } = await late.media.getMediaPresignedUrl({
47
+ body: {
48
+ filename: fileName,
49
+ contentType,
50
+ size: stat.size,
51
+ },
52
+ });
53
+ const presignData = presign;
54
+ // Upload file to presigned URL
55
+ const uploadRes = await fetch(presignData.uploadUrl, {
56
+ method: 'PUT',
57
+ headers: { 'Content-Type': contentType },
58
+ body: fileBuffer,
59
+ });
60
+ if (!uploadRes.ok) {
61
+ outputError(`Upload failed: ${uploadRes.statusText}`, uploadRes.status);
62
+ }
63
+ output({
64
+ success: true,
65
+ url: presignData.publicUrl,
66
+ key: presignData.key,
67
+ filename: fileName,
68
+ size: stat.size,
69
+ contentType,
70
+ }, argv.pretty);
71
+ }
72
+ catch (err) {
73
+ handleError(err);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,2 @@
1
+ import type { Argv } from 'yargs';
2
+ export declare function registerPlatformCommands(yargs: Argv): Argv;
@@ -0,0 +1,27 @@
1
+ import { output } from '../utils/output.js';
2
+ const platforms = [
3
+ 'twitter',
4
+ 'instagram',
5
+ 'facebook',
6
+ 'linkedin',
7
+ 'tiktok',
8
+ 'youtube',
9
+ 'pinterest',
10
+ 'reddit',
11
+ 'bluesky',
12
+ 'threads',
13
+ 'googlebusiness',
14
+ 'telegram',
15
+ 'snapchat',
16
+ 'whatsapp',
17
+ 'discord',
18
+ ];
19
+ export function registerPlatformCommands(yargs) {
20
+ return yargs.command('platforms:list', 'List supported platform identifiers', (y) => y, (argv) => {
21
+ output({
22
+ platforms,
23
+ source: 'https://docs.zernio.com/platforms',
24
+ note: 'Use the docs page as source of truth for current platform capabilities and constraints.',
25
+ }, argv.pretty);
26
+ });
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ /** Register post commands: posts:create, posts:list, posts:get, posts:delete, posts:retry */
3
+ export declare function registerPostCommands(yargs: Argv): Argv;