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,108 @@
1
+ import { openApiInfo } from '../generated/openapi-catalog.js';
2
+ import { parseJsonInput, parseKeyValueList } from '../utils/argument-parsing.js';
3
+ import { prepareApiRequest, runApiRequest } from '../utils/api-request.js';
4
+ import { requireApiKey } from '../utils/config.js';
5
+ import { handleError } from '../utils/errors.js';
6
+ import { endpointSuggestions, endpointToSummary, findEndpoint, listEndpoints, } from '../utils/openapi-catalog.js';
7
+ import { output, outputError } from '../utils/output.js';
8
+ export function registerApiCommands(yargs) {
9
+ return yargs
10
+ .command('api:catalog', 'Search the generated OpenAPI endpoint catalog', (y) => y
11
+ .option('tag', { type: 'string', describe: 'Filter by OpenAPI tag' })
12
+ .option('method', { type: 'string', describe: 'Filter by HTTP method' })
13
+ .option('search', { type: 'string', describe: 'Search operation IDs, paths, and summaries' })
14
+ .option('limit', { type: 'number', describe: 'Maximum endpoints to return', default: 50 }), (argv) => {
15
+ const endpoints = listEndpoints({
16
+ tag: argv.tag,
17
+ method: argv.method,
18
+ search: argv.search,
19
+ limit: argv.limit,
20
+ }).map(endpointToSummary);
21
+ output({ ...openApiInfo, endpoints }, argv.pretty);
22
+ })
23
+ .command('api:describe <operation>', 'Describe one endpoint by operationId or "METHOD /v1/path"', (y) => y.positional('operation', { type: 'string', demandOption: true }), (argv) => {
24
+ const endpoint = findEndpoint(argv.operation);
25
+ if (!endpoint) {
26
+ outputError(`Endpoint not found: ${argv.operation}`, 404);
27
+ }
28
+ output(endpoint, argv.pretty);
29
+ })
30
+ .command('api:call <operation>', 'Call any Zernio API endpoint from the OpenAPI catalog', (y) => y
31
+ .positional('operation', { type: 'string', demandOption: true })
32
+ .option('path', { type: 'array', describe: 'Path param key=value, repeatable' })
33
+ .option('query', { type: 'array', describe: 'Query param key=value, repeatable' })
34
+ .option('header', { type: 'array', describe: 'Request header key=value, repeatable' })
35
+ .option('form', { type: 'array', describe: 'Form field key=value, repeatable' })
36
+ .option('file', { type: 'array', describe: 'Multipart file key=/path/to/file, repeatable' })
37
+ .option('body-json', { type: 'string', describe: 'JSON request body string' })
38
+ .option('body-file', { type: 'string', describe: 'Path to JSON request body file' })
39
+ .option('raw-body-file', { type: 'string', describe: 'Path to raw request body file' })
40
+ .option('content-type', { type: 'string', describe: 'Content-Type for JSON, form, or raw body' })
41
+ .option('request-id', { type: 'string', describe: 'Set x-request-id header for safe retries' })
42
+ .option('idempotency-key', { type: 'string', describe: 'Set Idempotency-Key header for safe retries' })
43
+ .option('api-key', { type: 'string', describe: 'API key override; never printed' })
44
+ .option('base-url', { type: 'string', describe: 'API base URL override' })
45
+ .option('dry-run', { type: 'boolean', describe: 'Print request preview without sending' }), async (argv) => {
46
+ try {
47
+ const endpoint = findEndpoint(argv.operation);
48
+ if (!endpoint) {
49
+ outputError(`Endpoint not found: ${argv.operation}`, 404, { suggestions: endpointSuggestions(argv.operation).map(endpointToSummary) }, argv.pretty);
50
+ }
51
+ const headers = parseKeyValueList(argv.header);
52
+ if (argv.requestId)
53
+ headers['x-request-id'] = String(argv.requestId);
54
+ if (argv.idempotencyKey)
55
+ headers['Idempotency-Key'] = String(argv.idempotencyKey);
56
+ const requestOptions = {
57
+ apiKey: argv.apiKey,
58
+ baseUrl: argv.baseUrl,
59
+ method: endpoint.method,
60
+ path: endpoint.path,
61
+ headers,
62
+ pathParams: parseKeyValueList(argv.path),
63
+ query: parseKeyValueList(argv.query),
64
+ form: parseKeyValueList(argv.form),
65
+ files: parseKeyValueList(argv.file),
66
+ body: parseJsonInput(argv.bodyJson, argv.bodyFile),
67
+ rawBodyFile: argv.rawBodyFile,
68
+ contentType: argv.contentType,
69
+ };
70
+ if (!argv.dryRun && !argv.apiKey)
71
+ requireApiKey();
72
+ if (argv.dryRun) {
73
+ const prepared = prepareApiRequest(requestOptions);
74
+ const headers = prepared.init.headers;
75
+ output({
76
+ ok: true,
77
+ dryRun: true,
78
+ operation: endpointToSummary(endpoint),
79
+ request: {
80
+ method: requestOptions.method,
81
+ url: prepared.url,
82
+ hasAuthorization: headers.has('authorization'),
83
+ headers: headersToPreview(headers),
84
+ hasBody: prepared.init.body !== undefined,
85
+ },
86
+ }, argv.pretty);
87
+ return;
88
+ }
89
+ const result = await runApiRequest(requestOptions);
90
+ output(result, argv.pretty);
91
+ if (!result.ok)
92
+ process.exit(1);
93
+ }
94
+ catch (err) {
95
+ handleError(err);
96
+ }
97
+ });
98
+ }
99
+ function headersToPreview(headers) {
100
+ const preview = {};
101
+ headers.forEach((value, key) => {
102
+ preview[key] = isSafeHeaderValue(key) ? value : '<redacted>';
103
+ });
104
+ return preview;
105
+ }
106
+ function isSafeHeaderValue(key) {
107
+ return ['accept', 'content-type'].includes(key.toLowerCase());
108
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ /** Register auth commands: auth:login, auth:set, auth:check */
3
+ export declare function registerAuthCommands(yargs: Argv): Argv;
@@ -0,0 +1,138 @@
1
+ import { hostname } from 'os';
2
+ import { getConfig, writeConfig, requireApiKey } from '../utils/config.js';
3
+ import { output, outputError } from '../utils/output.js';
4
+ import { handleError } from '../utils/errors.js';
5
+ import Late from '@zernio/node';
6
+ /** Default API base URL (without /v1/ suffix, matches SDK default) */
7
+ const DEFAULT_BASE_URL = 'https://zernio.com/api';
8
+ /**
9
+ * Resolve the base URL for direct fetch calls (not through the SDK).
10
+ * The SDK adds /v1/ internally, but for auth endpoints we call non-SDK routes
11
+ * so we need the raw origin (e.g. https://zernio.com).
12
+ */
13
+ function getApiOrigin() {
14
+ const config = getConfig();
15
+ const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
16
+ // Strip trailing /api or /api/ to get the origin
17
+ return baseUrl.replace(/\/api\/?$/, '');
18
+ }
19
+ /**
20
+ * Sleep for the given number of milliseconds.
21
+ */
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ /** Register auth commands: auth:login, auth:set, auth:check */
26
+ export function registerAuthCommands(yargs) {
27
+ return yargs
28
+ .command('auth:login', 'Log in via browser (device authorization flow)', (y) => y.option('device-name', {
29
+ type: 'string',
30
+ describe: 'Device name for the API key label',
31
+ default: hostname(),
32
+ }), async (argv) => {
33
+ const origin = getApiOrigin();
34
+ const deviceName = argv['device-name'];
35
+ // Step 1: Initiate the device flow
36
+ let initiateRes;
37
+ try {
38
+ initiateRes = await fetch(`${origin}/api/auth/cli/initiate`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ deviceName }),
42
+ });
43
+ }
44
+ catch (err) {
45
+ outputError(`Failed to connect to ${origin}. Check your internet connection.`);
46
+ }
47
+ if (!initiateRes.ok) {
48
+ const data = await initiateRes.json().catch(() => ({}));
49
+ outputError(data.error || `Initiation failed (HTTP ${initiateRes.status})`);
50
+ }
51
+ const { deviceCode, userCode, browserUrl, expiresAt, interval } = await initiateRes.json();
52
+ // Step 2: Print instructions to stderr (so stdout stays clean for JSON)
53
+ process.stderr.write('\n');
54
+ process.stderr.write(` Confirmation code: ${userCode}\n\n`);
55
+ process.stderr.write(` Opening browser to: ${browserUrl}\n`);
56
+ process.stderr.write(` Waiting for authorization...\n\n`);
57
+ // Step 3: Open browser (dynamic import since 'open' is ESM-only)
58
+ try {
59
+ const open = (await import('open')).default;
60
+ await open(browserUrl);
61
+ }
62
+ catch {
63
+ process.stderr.write(` Could not open browser automatically.\n Visit this URL manually: ${browserUrl}\n\n`);
64
+ }
65
+ // Step 4: Poll for authorization
66
+ const pollInterval = (interval || 5) * 1000;
67
+ const deadline = new Date(expiresAt).getTime();
68
+ while (Date.now() < deadline) {
69
+ await sleep(pollInterval);
70
+ let pollRes;
71
+ try {
72
+ pollRes = await fetch(`${origin}/api/auth/cli/poll`, {
73
+ headers: { Authorization: `Bearer ${deviceCode}` },
74
+ });
75
+ }
76
+ catch {
77
+ // Network error, retry on next interval
78
+ continue;
79
+ }
80
+ // Session expired
81
+ if (pollRes.status === 410) {
82
+ outputError('Authorization session expired. Run `zernio auth:login` again.');
83
+ }
84
+ // Polling too fast (shouldn't happen, but handle gracefully)
85
+ if (pollRes.status === 429) {
86
+ continue;
87
+ }
88
+ if (!pollRes.ok) {
89
+ continue;
90
+ }
91
+ const data = await pollRes.json();
92
+ if (data.status === 'authorized' && data.apiKey) {
93
+ // Save the API key
94
+ writeConfig({ apiKey: data.apiKey });
95
+ process.stderr.write(' Authorized! API key saved to ~/.zernio/config.json\n\n');
96
+ output({ success: true, message: 'Logged in successfully. API key saved.' }, argv.pretty);
97
+ return;
98
+ }
99
+ if (data.status === 'denied') {
100
+ outputError('Authorization was denied.');
101
+ }
102
+ // status === 'pending', keep polling
103
+ }
104
+ // Timed out
105
+ outputError('Authorization timed out. Run `zernio auth:login` again.');
106
+ })
107
+ .command('auth:set', 'Save API key to ~/.zernio/config.json', (y) => y
108
+ .option('key', {
109
+ type: 'string',
110
+ describe: 'API key',
111
+ demandOption: true,
112
+ })
113
+ .option('url', {
114
+ type: 'string',
115
+ describe: 'Custom API base URL',
116
+ }), async (argv) => {
117
+ const updates = { apiKey: argv.key };
118
+ if (argv.url)
119
+ updates.baseUrl = argv.url;
120
+ writeConfig(updates);
121
+ output({ success: true, message: 'API key saved to ~/.zernio/config.json' }, argv.pretty);
122
+ })
123
+ .command('auth:check', 'Verify API key works', (y) => y, async (argv) => {
124
+ try {
125
+ const apiKey = requireApiKey();
126
+ const config = getConfig();
127
+ const late = new Late({
128
+ apiKey,
129
+ baseURL: config.baseUrl || undefined,
130
+ });
131
+ const { data } = await late.users.listUsers();
132
+ output({ success: true, message: 'API key is valid', ...data }, argv.pretty);
133
+ }
134
+ catch (err) {
135
+ handleError(err);
136
+ }
137
+ });
138
+ }
@@ -0,0 +1,5 @@
1
+ import type { Argv } from 'yargs';
2
+ /**
3
+ * Register comment-to-DM automation commands: CRUD and logs.
4
+ */
5
+ export declare function registerAutomationCommands(yargs: Argv): Argv;
@@ -0,0 +1,139 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /**
5
+ * Register comment-to-DM automation commands: CRUD and logs.
6
+ */
7
+ export function registerAutomationCommands(yargs) {
8
+ return yargs
9
+ .command('automations:list', 'List comment-to-DM automations', (y) => y
10
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' }), async (argv) => {
11
+ try {
12
+ const late = createClient();
13
+ const query = {};
14
+ if (argv.profileId)
15
+ query.profileId = argv.profileId;
16
+ const { data } = await late.commentautomations.listCommentAutomations({ query });
17
+ output(data, argv.pretty);
18
+ }
19
+ catch (err) {
20
+ handleError(err);
21
+ }
22
+ })
23
+ .command('automations:create', 'Create a comment-to-DM automation', (y) => y
24
+ .option('profileId', { type: 'string', describe: 'Profile ID', demandOption: true })
25
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
26
+ .option('platformPostId', { type: 'string', describe: 'Platform-specific post ID', demandOption: true })
27
+ .option('name', { type: 'string', describe: 'Automation name', demandOption: true })
28
+ .option('dmMessage', { type: 'string', describe: 'DM message to send', demandOption: true })
29
+ .option('postId', { type: 'string', describe: 'Zernio post ID (optional)' })
30
+ .option('postTitle', { type: 'string', describe: 'Post title for display' })
31
+ .option('keywords', { type: 'string', describe: 'Comma-separated trigger keywords (empty = all comments)' })
32
+ .option('matchMode', { type: 'string', describe: 'Keyword match mode (exact, contains)', default: 'contains' })
33
+ .option('commentReply', { type: 'string', describe: 'Optional public comment reply text' }), async (argv) => {
34
+ try {
35
+ const late = createClient();
36
+ const body = {
37
+ profileId: argv.profileId,
38
+ accountId: argv.accountId,
39
+ platformPostId: argv.platformPostId,
40
+ name: argv.name,
41
+ dmMessage: argv.dmMessage,
42
+ matchMode: argv.matchMode,
43
+ };
44
+ if (argv.postId)
45
+ body.postId = argv.postId;
46
+ if (argv.postTitle)
47
+ body.postTitle = argv.postTitle;
48
+ if (argv.keywords)
49
+ body.keywords = argv.keywords.split(',').map((s) => s.trim());
50
+ if (argv.commentReply)
51
+ body.commentReply = argv.commentReply;
52
+ const { data } = await late.commentautomations.createCommentAutomation({ body: body });
53
+ output(data, argv.pretty);
54
+ }
55
+ catch (err) {
56
+ handleError(err);
57
+ }
58
+ })
59
+ .command('automations:get <id>', 'Get automation details with recent logs', (y) => y.positional('id', { type: 'string', describe: 'Automation ID', demandOption: true }), async (argv) => {
60
+ try {
61
+ const late = createClient();
62
+ const { data } = await late.commentautomations.getCommentAutomation({
63
+ path: { automationId: argv.id },
64
+ });
65
+ output(data, argv.pretty);
66
+ }
67
+ catch (err) {
68
+ handleError(err);
69
+ }
70
+ })
71
+ .command('automations:update <id>', 'Update an automation', (y) => y
72
+ .positional('id', { type: 'string', describe: 'Automation ID', demandOption: true })
73
+ .option('name', { type: 'string', describe: 'Automation name' })
74
+ .option('keywords', { type: 'string', describe: 'Comma-separated trigger keywords' })
75
+ .option('matchMode', { type: 'string', describe: 'Keyword match mode (exact, contains)' })
76
+ .option('dmMessage', { type: 'string', describe: 'DM message to send' })
77
+ .option('commentReply', { type: 'string', describe: 'Public comment reply text' })
78
+ .option('isActive', { type: 'boolean', describe: 'Enable or disable the automation' }), async (argv) => {
79
+ try {
80
+ const late = createClient();
81
+ const body = {};
82
+ if (argv.name)
83
+ body.name = argv.name;
84
+ if (argv.keywords)
85
+ body.keywords = argv.keywords.split(',').map((s) => s.trim());
86
+ if (argv.matchMode)
87
+ body.matchMode = argv.matchMode;
88
+ if (argv.dmMessage)
89
+ body.dmMessage = argv.dmMessage;
90
+ if (argv.commentReply)
91
+ body.commentReply = argv.commentReply;
92
+ if (argv.isActive !== undefined)
93
+ body.isActive = argv.isActive;
94
+ const { data } = await late.commentautomations.updateCommentAutomation({
95
+ path: { automationId: argv.id },
96
+ body: body,
97
+ });
98
+ output(data, argv.pretty);
99
+ }
100
+ catch (err) {
101
+ handleError(err);
102
+ }
103
+ })
104
+ .command('automations:delete <id>', 'Delete an automation and all its logs', (y) => y.positional('id', { type: 'string', describe: 'Automation ID', demandOption: true }), async (argv) => {
105
+ try {
106
+ const late = createClient();
107
+ const { data } = await late.commentautomations.deleteCommentAutomation({
108
+ path: { automationId: argv.id },
109
+ });
110
+ output(data, argv.pretty);
111
+ }
112
+ catch (err) {
113
+ handleError(err);
114
+ }
115
+ })
116
+ .command('automations:logs <id>', 'List trigger logs for an automation', (y) => y
117
+ .positional('id', { type: 'string', describe: 'Automation ID', demandOption: true })
118
+ .option('status', { type: 'string', describe: 'Filter by status (sent, failed, skipped)' })
119
+ .option('limit', { type: 'number', describe: 'Max results', default: 50 })
120
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
121
+ try {
122
+ const late = createClient();
123
+ const query = {
124
+ limit: argv.limit,
125
+ skip: argv.skip,
126
+ };
127
+ if (argv.status)
128
+ query.status = argv.status;
129
+ const { data } = await late.commentautomations.listCommentAutomationLogs({
130
+ path: { automationId: argv.id },
131
+ query,
132
+ });
133
+ output(data, argv.pretty);
134
+ }
135
+ catch (err) {
136
+ handleError(err);
137
+ }
138
+ });
139
+ }
@@ -0,0 +1,5 @@
1
+ import type { Argv } from 'yargs';
2
+ /**
3
+ * Register broadcast commands: CRUD, send, schedule, cancel, recipients.
4
+ */
5
+ export declare function registerBroadcastCommands(yargs: Argv): Argv;
@@ -0,0 +1,184 @@
1
+ import { createClient } from '../client.js';
2
+ import { output } from '../utils/output.js';
3
+ import { handleError } from '../utils/errors.js';
4
+ /**
5
+ * Register broadcast commands: CRUD, send, schedule, cancel, recipients.
6
+ */
7
+ export function registerBroadcastCommands(yargs) {
8
+ return yargs
9
+ .command('broadcasts:list', 'List broadcasts', (y) => y
10
+ .option('profileId', { type: 'string', describe: 'Filter by profile ID' })
11
+ .option('status', { type: 'string', describe: 'Filter by status (draft, scheduled, sending, completed, failed, cancelled)' })
12
+ .option('platform', { type: 'string', describe: 'Filter by platform' })
13
+ .option('limit', { type: 'number', describe: 'Max results', default: 20 })
14
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
15
+ try {
16
+ const late = createClient();
17
+ const query = {
18
+ limit: argv.limit,
19
+ skip: argv.skip,
20
+ };
21
+ if (argv.profileId)
22
+ query.profileId = argv.profileId;
23
+ if (argv.status)
24
+ query.status = argv.status;
25
+ if (argv.platform)
26
+ query.platform = argv.platform;
27
+ const { data } = await late.broadcasts.listBroadcasts({ query });
28
+ output(data, argv.pretty);
29
+ }
30
+ catch (err) {
31
+ handleError(err);
32
+ }
33
+ })
34
+ .command('broadcasts:create', 'Create a broadcast draft', (y) => y
35
+ .option('profileId', { type: 'string', describe: 'Profile ID', demandOption: true })
36
+ .option('accountId', { type: 'string', describe: 'Account ID', demandOption: true })
37
+ .option('platform', { type: 'string', describe: 'Platform', demandOption: true })
38
+ .option('name', { type: 'string', describe: 'Broadcast name', demandOption: true })
39
+ .option('message', { type: 'string', describe: 'Message text (for non-WhatsApp platforms)' })
40
+ .option('templateName', { type: 'string', describe: 'WhatsApp template name' })
41
+ .option('templateLanguage', { type: 'string', describe: 'WhatsApp template language code' }), async (argv) => {
42
+ try {
43
+ const late = createClient();
44
+ const body = {
45
+ profileId: argv.profileId,
46
+ accountId: argv.accountId,
47
+ platform: argv.platform,
48
+ name: argv.name,
49
+ };
50
+ // WhatsApp uses templates, other platforms use direct messages
51
+ if (argv.templateName) {
52
+ body.template = {
53
+ name: argv.templateName,
54
+ language: argv.templateLanguage || 'en',
55
+ };
56
+ }
57
+ else if (argv.message) {
58
+ body.message = { text: argv.message };
59
+ }
60
+ const { data } = await late.broadcasts.createBroadcast({ body: body });
61
+ output(data, argv.pretty);
62
+ }
63
+ catch (err) {
64
+ handleError(err);
65
+ }
66
+ })
67
+ .command('broadcasts:get <id>', 'Get broadcast details with stats', (y) => y.positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true }), async (argv) => {
68
+ try {
69
+ const late = createClient();
70
+ const { data } = await late.broadcasts.getBroadcast({ path: { broadcastId: argv.id } });
71
+ output(data, argv.pretty);
72
+ }
73
+ catch (err) {
74
+ handleError(err);
75
+ }
76
+ })
77
+ .command('broadcasts:update <id>', 'Update a broadcast (draft only)', (y) => y
78
+ .positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true })
79
+ .option('name', { type: 'string', describe: 'Broadcast name' })
80
+ .option('message', { type: 'string', describe: 'Message text' }), async (argv) => {
81
+ try {
82
+ const late = createClient();
83
+ const { data } = await late.broadcasts.updateBroadcast({
84
+ path: { broadcastId: argv.id },
85
+ });
86
+ output(data, argv.pretty);
87
+ }
88
+ catch (err) {
89
+ handleError(err);
90
+ }
91
+ })
92
+ .command('broadcasts:delete <id>', 'Delete a broadcast (draft only)', (y) => y.positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true }), async (argv) => {
93
+ try {
94
+ const late = createClient();
95
+ const { data } = await late.broadcasts.deleteBroadcast({ path: { broadcastId: argv.id } });
96
+ output(data, argv.pretty);
97
+ }
98
+ catch (err) {
99
+ handleError(err);
100
+ }
101
+ })
102
+ .command('broadcasts:send <id>', 'Send a broadcast immediately', (y) => y.positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true }), async (argv) => {
103
+ try {
104
+ const late = createClient();
105
+ const { data } = await late.broadcasts.sendBroadcast({ path: { broadcastId: argv.id } });
106
+ output(data, argv.pretty);
107
+ }
108
+ catch (err) {
109
+ handleError(err);
110
+ }
111
+ })
112
+ .command('broadcasts:schedule <id>', 'Schedule a broadcast for later', (y) => y
113
+ .positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true })
114
+ .option('scheduledAt', { type: 'string', describe: 'ISO 8601 date to send at', demandOption: true }), async (argv) => {
115
+ try {
116
+ const late = createClient();
117
+ const { data } = await late.broadcasts.scheduleBroadcast({
118
+ path: { broadcastId: argv.id },
119
+ body: { scheduledAt: argv.scheduledAt },
120
+ });
121
+ output(data, argv.pretty);
122
+ }
123
+ catch (err) {
124
+ handleError(err);
125
+ }
126
+ })
127
+ .command('broadcasts:cancel <id>', 'Cancel a scheduled or sending broadcast', (y) => y.positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true }), async (argv) => {
128
+ try {
129
+ const late = createClient();
130
+ const { data } = await late.broadcasts.cancelBroadcast({ path: { broadcastId: argv.id } });
131
+ output(data, argv.pretty);
132
+ }
133
+ catch (err) {
134
+ handleError(err);
135
+ }
136
+ })
137
+ .command('broadcasts:recipients <id>', 'List broadcast recipients', (y) => y
138
+ .positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true })
139
+ .option('status', { type: 'string', describe: 'Filter by status (pending, sent, delivered, read, failed)' })
140
+ .option('limit', { type: 'number', describe: 'Max results', default: 50 })
141
+ .option('skip', { type: 'number', describe: 'Skip N results', default: 0 }), async (argv) => {
142
+ try {
143
+ const late = createClient();
144
+ const query = {
145
+ limit: argv.limit,
146
+ skip: argv.skip,
147
+ };
148
+ if (argv.status)
149
+ query.status = argv.status;
150
+ const { data } = await late.broadcasts.listBroadcastRecipients({
151
+ path: { broadcastId: argv.id },
152
+ query,
153
+ });
154
+ output(data, argv.pretty);
155
+ }
156
+ catch (err) {
157
+ handleError(err);
158
+ }
159
+ })
160
+ .command('broadcasts:add-recipients <id>', 'Add recipients to a broadcast', (y) => y
161
+ .positional('id', { type: 'string', describe: 'Broadcast ID', demandOption: true })
162
+ .option('contactIds', { type: 'string', describe: 'Comma-separated contact IDs' })
163
+ .option('phones', { type: 'string', describe: 'Comma-separated phone numbers (WhatsApp/Telegram)' })
164
+ .option('useSegment', { type: 'boolean', describe: 'Use broadcast segment filters to auto-add contacts' }), async (argv) => {
165
+ try {
166
+ const late = createClient();
167
+ const body = {};
168
+ if (argv.contactIds)
169
+ body.contactIds = argv.contactIds.split(',').map((s) => s.trim());
170
+ if (argv.phones)
171
+ body.phones = argv.phones.split(',').map((s) => s.trim());
172
+ if (argv.useSegment)
173
+ body.useSegment = true;
174
+ const { data } = await late.broadcasts.addBroadcastRecipients({
175
+ path: { broadcastId: argv.id },
176
+ body: body,
177
+ });
178
+ output(data, argv.pretty);
179
+ }
180
+ catch (err) {
181
+ handleError(err);
182
+ }
183
+ });
184
+ }
@@ -0,0 +1,6 @@
1
+ import type { Argv } from 'yargs';
2
+ /**
3
+ * Register contact commands: contacts:list, contacts:create, contacts:get, contacts:update,
4
+ * contacts:delete, contacts:channels, contacts:set-field, contacts:clear-field, contacts:bulk-create.
5
+ */
6
+ export declare function registerContactCommands(yargs: Argv): Argv;