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,129 @@
1
+ import { createClient } from '../client.js';
2
+ import { output, outputError } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /** Register post commands: posts:create, posts:list, posts:get, posts:delete, posts:retry */
5
+ 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
+ })
68
+ .command('posts:list', 'List posts', (y) => y
69
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
70
+ .option('status', { type: 'string', describe: 'Filter by status (scheduled, published, failed, draft)' })
71
+ .option('platform', { type: 'string', describe: 'Filter by platform' })
72
+ .option('from', { type: 'string', describe: 'Start date (ISO 8601)' })
73
+ .option('to', { type: 'string', describe: 'End date (ISO 8601)' })
74
+ .option('page', { type: 'number', describe: 'Page number', default: 1 })
75
+ .option('limit', { type: 'number', describe: 'Results per page', default: 10 }), async (argv) => {
76
+ try {
77
+ const late = createClient();
78
+ const query = {
79
+ page: argv.page,
80
+ limit: argv.limit,
81
+ };
82
+ if (argv.profileId)
83
+ query.profileId = argv.profileId;
84
+ if (argv.status)
85
+ query.status = argv.status;
86
+ if (argv.platform)
87
+ query.platform = argv.platform;
88
+ if (argv.from)
89
+ query.dateFrom = argv.from;
90
+ if (argv.to)
91
+ query.dateTo = argv.to;
92
+ const { data } = await late.posts.listPosts({ query });
93
+ output(data, argv.pretty);
94
+ }
95
+ catch (err) {
96
+ handleError(err);
97
+ }
98
+ })
99
+ .command('posts:get <id>', 'Get post details', (y) => y.positional('id', { type: 'string', describe: 'Post ID', demandOption: true }), async (argv) => {
100
+ try {
101
+ const late = createClient();
102
+ const { data } = await late.posts.getPost({ path: { postId: argv.id } });
103
+ output(data, argv.pretty);
104
+ }
105
+ catch (err) {
106
+ handleError(err);
107
+ }
108
+ })
109
+ .command('posts:delete <id>', 'Delete a post', (y) => y.positional('id', { type: 'string', describe: 'Post ID', demandOption: true }), async (argv) => {
110
+ try {
111
+ const late = createClient();
112
+ const { data } = await late.posts.deletePost({ path: { postId: argv.id } });
113
+ output(data, argv.pretty);
114
+ }
115
+ catch (err) {
116
+ handleError(err);
117
+ }
118
+ })
119
+ .command('posts:retry <id>', 'Retry a failed post', (y) => y.positional('id', { type: 'string', describe: 'Post ID', demandOption: true }), async (argv) => {
120
+ try {
121
+ const late = createClient();
122
+ const { data } = await late.posts.retryPost({ path: { postId: argv.id } });
123
+ output(data, argv.pretty);
124
+ }
125
+ catch (err) {
126
+ handleError(err);
127
+ }
128
+ });
129
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ /** Register profile commands: profiles:list, profiles:create, profiles:get, profiles:update, profiles:delete */
3
+ export declare function registerProfileCommands(yargs: Argv): Argv;
@@ -0,0 +1,82 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /** Register profile commands: profiles:list, profiles:create, profiles:get, profiles:update, profiles:delete */
5
+ export function registerProfileCommands(yargs) {
6
+ return yargs
7
+ .command('profiles:list', 'List all profiles', (y) => y, async (argv) => {
8
+ try {
9
+ const late = createClient();
10
+ const { data } = await late.profiles.listProfiles();
11
+ output(data, argv.pretty);
12
+ }
13
+ catch (err) {
14
+ handleError(err);
15
+ }
16
+ })
17
+ .command('profiles:create', 'Create a new profile', (y) => y
18
+ .option('name', { type: 'string', describe: 'Profile name', demandOption: true })
19
+ .option('description', { type: 'string', describe: 'Profile description' })
20
+ .option('color', { type: 'string', describe: 'Profile color' }), async (argv) => {
21
+ try {
22
+ const late = createClient();
23
+ const body = { name: argv.name };
24
+ if (argv.description)
25
+ body.description = argv.description;
26
+ if (argv.color)
27
+ body.color = argv.color;
28
+ const { data } = await late.profiles.createProfile({ body });
29
+ output(data, argv.pretty);
30
+ }
31
+ catch (err) {
32
+ handleError(err);
33
+ }
34
+ })
35
+ .command('profiles:get <id>', 'Get profile details', (y) => y.positional('id', { type: 'string', describe: 'Profile ID', demandOption: true }), async (argv) => {
36
+ try {
37
+ const late = createClient();
38
+ const { data } = await late.profiles.getProfile({ path: { profileId: argv.id } });
39
+ output(data, argv.pretty);
40
+ }
41
+ catch (err) {
42
+ handleError(err);
43
+ }
44
+ })
45
+ .command('profiles:update <id>', 'Update a profile', (y) => y
46
+ .positional('id', { type: 'string', describe: 'Profile ID', demandOption: true })
47
+ .option('name', { type: 'string', describe: 'New name' })
48
+ .option('description', { type: 'string', describe: 'New description' })
49
+ .option('color', { type: 'string', describe: 'New color' })
50
+ .option('isDefault', { type: 'boolean', describe: 'Set as default profile' }), async (argv) => {
51
+ try {
52
+ const late = createClient();
53
+ const body = {};
54
+ if (argv.name !== undefined)
55
+ body.name = argv.name;
56
+ if (argv.description !== undefined)
57
+ body.description = argv.description;
58
+ if (argv.color !== undefined)
59
+ body.color = argv.color;
60
+ if (argv.isDefault !== undefined)
61
+ body.isDefault = argv.isDefault;
62
+ const { data } = await late.profiles.updateProfile({
63
+ path: { profileId: argv.id },
64
+ body: body,
65
+ });
66
+ output(data, argv.pretty);
67
+ }
68
+ catch (err) {
69
+ handleError(err);
70
+ }
71
+ })
72
+ .command('profiles:delete <id>', 'Delete a profile', (y) => y.positional('id', { type: 'string', describe: 'Profile ID', demandOption: true }), async (argv) => {
73
+ try {
74
+ const late = createClient();
75
+ const { data } = await late.profiles.deleteProfile({ path: { profileId: argv.id } });
76
+ output(data, argv.pretty);
77
+ }
78
+ catch (err) {
79
+ handleError(err);
80
+ }
81
+ });
82
+ }
@@ -0,0 +1,5 @@
1
+ import type { Argv } from 'yargs';
2
+ /**
3
+ * Register sequence commands: CRUD, activate, pause, enroll, unenroll, enrollments.
4
+ */
5
+ export declare function registerSequenceCommands(yargs: Argv): Argv;
@@ -0,0 +1,178 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /**
5
+ * Register sequence commands: CRUD, activate, pause, enroll, unenroll, enrollments.
6
+ */
7
+ export function registerSequenceCommands(yargs) {
8
+ return yargs
9
+ .command('sequences:list', 'List sequences', (y) => y
10
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
11
+ .option('status', { type: 'string', describe: 'Filter by status (draft, active, paused)' })
12
+ .option('limit', { type: 'number', describe: 'Max results', default: 20 })
13
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
14
+ try {
15
+ const late = createClient();
16
+ const query = {
17
+ limit: argv.limit,
18
+ skip: argv.skip,
19
+ };
20
+ if (argv.profileId)
21
+ query.profileId = argv.profileId;
22
+ if (argv.status)
23
+ query.status = argv.status;
24
+ const { data } = await late.sequences.listSequences({ query });
25
+ output(data, argv.pretty);
26
+ }
27
+ catch (err) {
28
+ handleError(err);
29
+ }
30
+ })
31
+ .command('sequences:create', 'Create a sequence', (y) => y
32
+ .option('profileId', { type: 'string', describe: 'Profile ID', demandOption: true })
33
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
34
+ .option('platform', { type: 'string', describe: 'Platform', demandOption: true })
35
+ .option('name', { type: 'string', describe: 'Sequence name', demandOption: true })
36
+ .option('stepsFile', { type: 'string', describe: 'Path to JSON file with steps array' })
37
+ .option('exitOnReply', { type: 'boolean', describe: 'Exit sequence when contact replies', default: true })
38
+ .option('exitOnUnsubscribe', { type: 'boolean', describe: 'Exit sequence when contact unsubscribes', default: true }), async (argv) => {
39
+ try {
40
+ const late = createClient();
41
+ const body = {
42
+ profileId: argv.profileId,
43
+ accountId: argv.accountId,
44
+ platform: argv.platform,
45
+ name: argv.name,
46
+ exitOnReply: argv.exitOnReply,
47
+ exitOnUnsubscribe: argv.exitOnUnsubscribe,
48
+ };
49
+ // Steps can be provided via a JSON file for complex sequences
50
+ if (argv.stepsFile) {
51
+ const { readFileSync } = await import('fs');
52
+ const raw = readFileSync(argv.stepsFile, 'utf-8');
53
+ body.steps = JSON.parse(raw);
54
+ }
55
+ const { data } = await late.sequences.createSequence({ body: body });
56
+ output(data, argv.pretty);
57
+ }
58
+ catch (err) {
59
+ handleError(err);
60
+ }
61
+ })
62
+ .command('sequences:get <id>', 'Get sequence details with steps', (y) => y.positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true }), async (argv) => {
63
+ try {
64
+ const late = createClient();
65
+ const { data } = await late.sequences.getSequence({ path: { sequenceId: argv.id } });
66
+ output(data, argv.pretty);
67
+ }
68
+ catch (err) {
69
+ handleError(err);
70
+ }
71
+ })
72
+ .command('sequences:update <id>', 'Update a sequence', (y) => y
73
+ .positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true })
74
+ .option('name', { type: 'string', describe: 'Sequence name' })
75
+ .option('stepsFile', { type: 'string', describe: 'Path to JSON file with steps array' })
76
+ .option('exitOnReply', { type: 'boolean', describe: 'Exit on reply' })
77
+ .option('exitOnUnsubscribe', { type: 'boolean', describe: 'Exit on unsubscribe' }), async (argv) => {
78
+ try {
79
+ const late = createClient();
80
+ const { data } = await late.sequences.updateSequence({
81
+ path: { sequenceId: argv.id },
82
+ });
83
+ output(data, argv.pretty);
84
+ }
85
+ catch (err) {
86
+ handleError(err);
87
+ }
88
+ })
89
+ .command('sequences:delete <id>', 'Delete a sequence', (y) => y.positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true }), async (argv) => {
90
+ try {
91
+ const late = createClient();
92
+ const { data } = await late.sequences.deleteSequence({ path: { sequenceId: argv.id } });
93
+ output(data, argv.pretty);
94
+ }
95
+ catch (err) {
96
+ handleError(err);
97
+ }
98
+ })
99
+ .command('sequences:activate <id>', 'Activate a sequence', (y) => y.positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true }), async (argv) => {
100
+ try {
101
+ const late = createClient();
102
+ const { data } = await late.sequences.activateSequence({ path: { sequenceId: argv.id } });
103
+ output(data, argv.pretty);
104
+ }
105
+ catch (err) {
106
+ handleError(err);
107
+ }
108
+ })
109
+ .command('sequences:pause <id>', 'Pause a sequence', (y) => y.positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true }), async (argv) => {
110
+ try {
111
+ const late = createClient();
112
+ const { data } = await late.sequences.pauseSequence({ path: { sequenceId: argv.id } });
113
+ output(data, argv.pretty);
114
+ }
115
+ catch (err) {
116
+ handleError(err);
117
+ }
118
+ })
119
+ .command('sequences:enroll <id>', 'Enroll contacts into a sequence', (y) => y
120
+ .positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true })
121
+ .option('contactIds', { type: 'string', describe: 'Comma-separated contact IDs', demandOption: true })
122
+ .option('channelIds', { type: 'string', describe: 'Comma-separated channel IDs (optional, uses default channel per contact)' }), async (argv) => {
123
+ try {
124
+ const late = createClient();
125
+ const body = {
126
+ contactIds: argv.contactIds.split(',').map((s) => s.trim()),
127
+ };
128
+ if (argv.channelIds) {
129
+ body.channelIds = argv.channelIds.split(',').map((s) => s.trim());
130
+ }
131
+ const { data } = await late.sequences.enrollContacts({
132
+ path: { sequenceId: argv.id },
133
+ body: body,
134
+ });
135
+ output(data, argv.pretty);
136
+ }
137
+ catch (err) {
138
+ handleError(err);
139
+ }
140
+ })
141
+ .command('sequences:unenroll <id> <contactId>', 'Unenroll a contact from a sequence', (y) => y
142
+ .positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true })
143
+ .positional('contactId', { type: 'string', describe: 'Contact ID', demandOption: true }), async (argv) => {
144
+ try {
145
+ const late = createClient();
146
+ const { data } = await late.sequences.unenrollContact({
147
+ path: { sequenceId: argv.id, contactId: argv.contactId },
148
+ });
149
+ output(data, argv.pretty);
150
+ }
151
+ catch (err) {
152
+ handleError(err);
153
+ }
154
+ })
155
+ .command('sequences:enrollments <id>', 'List enrollments for a sequence', (y) => y
156
+ .positional('id', { type: 'string', describe: 'Sequence ID', demandOption: true })
157
+ .option('status', { type: 'string', describe: 'Filter by status (active, completed, exited, paused)' })
158
+ .option('limit', { type: 'number', describe: 'Max results', default: 50 })
159
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
160
+ try {
161
+ const late = createClient();
162
+ const query = {
163
+ limit: argv.limit,
164
+ skip: argv.skip,
165
+ };
166
+ if (argv.status)
167
+ query.status = argv.status;
168
+ const { data } = await late.sequences.listSequenceEnrollments({
169
+ path: { sequenceId: argv.id },
170
+ query,
171
+ });
172
+ output(data, argv.pretty);
173
+ }
174
+ catch (err) {
175
+ handleError(err);
176
+ }
177
+ });
178
+ }