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.
- package/README.md +145 -0
- package/SKILL.md +98 -0
- package/claude/plugin.json +23 -0
- package/claude/skills/zernio/SKILL.md +83 -0
- package/claude/skills/zernio/references/zernio-api-surface.md +48 -0
- package/claude/skills/zernio/references/zernio-best-practices.md +33 -0
- package/claude/skills/zernio/references/zernio-workflows.md +58 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +14 -0
- package/dist/commands/accounts.d.ts +3 -0
- package/dist/commands/accounts.js +53 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +85 -0
- package/dist/commands/api.d.ts +2 -0
- package/dist/commands/api.js +108 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +138 -0
- package/dist/commands/automations.d.ts +5 -0
- package/dist/commands/automations.js +139 -0
- package/dist/commands/broadcasts.d.ts +5 -0
- package/dist/commands/broadcasts.js +184 -0
- package/dist/commands/contacts.d.ts +6 -0
- package/dist/commands/contacts.js +198 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +51 -0
- package/dist/commands/inbox.d.ts +6 -0
- package/dist/commands/inbox.js +222 -0
- package/dist/commands/media.d.ts +3 -0
- package/dist/commands/media.js +76 -0
- package/dist/commands/platforms.d.ts +2 -0
- package/dist/commands/platforms.js +27 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +129 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +82 -0
- package/dist/commands/sequences.d.ts +5 -0
- package/dist/commands/sequences.js +178 -0
- package/dist/generated/openapi-catalog.d.ts +8961 -0
- package/dist/generated/openapi-catalog.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +61 -0
- package/dist/utils/api-request.d.ts +31 -0
- package/dist/utils/api-request.js +115 -0
- package/dist/utils/argument-parsing.d.ts +3 -0
- package/dist/utils/argument-parsing.js +27 -0
- package/dist/utils/config.d.ts +27 -0
- package/dist/utils/config.js +111 -0
- package/dist/utils/errors.d.ts +5 -0
- package/dist/utils/errors.js +12 -0
- package/dist/utils/openapi-catalog.d.ts +16 -0
- package/dist/utils/openapi-catalog.js +58 -0
- package/dist/utils/output.d.ts +9 -0
- package/dist/utils/output.js +24 -0
- package/docs/architecture.md +37 -0
- package/docs/cli.md +99 -0
- package/docs/code-standards.md +11 -0
- package/docs/contributing.md +34 -0
- package/docs/development-roadmap.md +32 -0
- package/docs/openapi/zernio-api-openapi.yaml +30967 -0
- package/docs/project-changelog.md +15 -0
- package/docs/project-overview-pdr.md +25 -0
- package/docs/system-architecture.md +28 -0
- package/package.json +82 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
import { registerAuthCommands } from './commands/auth.js';
|
|
5
|
+
import { registerProfileCommands } from './commands/profiles.js';
|
|
6
|
+
import { registerAccountCommands } from './commands/accounts.js';
|
|
7
|
+
import { registerPostCommands } from './commands/posts.js';
|
|
8
|
+
import { registerAnalyticsCommands } from './commands/analytics.js';
|
|
9
|
+
import { registerMediaCommands } from './commands/media.js';
|
|
10
|
+
import { registerInboxCommands } from './commands/inbox.js';
|
|
11
|
+
import { registerContactCommands } from './commands/contacts.js';
|
|
12
|
+
import { registerBroadcastCommands } from './commands/broadcasts.js';
|
|
13
|
+
import { registerSequenceCommands } from './commands/sequences.js';
|
|
14
|
+
import { registerAutomationCommands } from './commands/automations.js';
|
|
15
|
+
import { registerApiCommands } from './commands/api.js';
|
|
16
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
17
|
+
import { registerPlatformCommands } from './commands/platforms.js';
|
|
18
|
+
/**
|
|
19
|
+
* Zernio CLI - Schedule posts, manage inbox, broadcasts, sequences, and automations across 14 platforms.
|
|
20
|
+
*
|
|
21
|
+
* Outputs JSON by default (optimized for AI agents and piping).
|
|
22
|
+
* Use --pretty for human-readable indented JSON.
|
|
23
|
+
*/
|
|
24
|
+
let cli = yargs(hideBin(process.argv))
|
|
25
|
+
.scriptName('zernio')
|
|
26
|
+
.usage('Usage: zernio <command> [options]')
|
|
27
|
+
.option('pretty', {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
describe: 'Pretty-print JSON output',
|
|
30
|
+
default: false,
|
|
31
|
+
global: true,
|
|
32
|
+
})
|
|
33
|
+
.option('json', {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
describe: 'Keep machine-readable JSON output (default behavior)',
|
|
36
|
+
default: true,
|
|
37
|
+
global: true,
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.demandCommand(1, 'You need to specify a command. Run "zernio --help" for available commands.')
|
|
41
|
+
.help()
|
|
42
|
+
.alias('h', 'help')
|
|
43
|
+
.version()
|
|
44
|
+
.alias('v', 'version');
|
|
45
|
+
// Register all command groups
|
|
46
|
+
cli = registerAuthCommands(cli);
|
|
47
|
+
cli = registerProfileCommands(cli);
|
|
48
|
+
cli = registerAccountCommands(cli);
|
|
49
|
+
cli = registerPostCommands(cli);
|
|
50
|
+
cli = registerAnalyticsCommands(cli);
|
|
51
|
+
cli = registerMediaCommands(cli);
|
|
52
|
+
cli = registerInboxCommands(cli);
|
|
53
|
+
cli = registerContactCommands(cli);
|
|
54
|
+
cli = registerBroadcastCommands(cli);
|
|
55
|
+
cli = registerSequenceCommands(cli);
|
|
56
|
+
cli = registerAutomationCommands(cli);
|
|
57
|
+
cli = registerApiCommands(cli);
|
|
58
|
+
cli = registerDoctorCommand(cli);
|
|
59
|
+
cli = registerPlatformCommands(cli);
|
|
60
|
+
// Parse and execute
|
|
61
|
+
cli.parse();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ApiRequestOptions {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
pathParams?: Record<string, string>;
|
|
8
|
+
query?: Record<string, string>;
|
|
9
|
+
body?: unknown;
|
|
10
|
+
form?: Record<string, string>;
|
|
11
|
+
files?: Record<string, string>;
|
|
12
|
+
rawBodyFile?: string;
|
|
13
|
+
contentType?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PreparedApiRequest {
|
|
16
|
+
url: string;
|
|
17
|
+
init: RequestInit;
|
|
18
|
+
}
|
|
19
|
+
export declare function prepareApiRequest(options: ApiRequestOptions): PreparedApiRequest;
|
|
20
|
+
export declare function runApiRequest(options: ApiRequestOptions): Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
statusText: string;
|
|
24
|
+
rateLimit: {
|
|
25
|
+
limit: string | null;
|
|
26
|
+
remaining: string | null;
|
|
27
|
+
reset: string | null;
|
|
28
|
+
retryAfter: string | null;
|
|
29
|
+
};
|
|
30
|
+
data: any;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { resolveConfig } from './config.js';
|
|
4
|
+
const DEFAULT_BASE_URL = 'https://zernio.com/api';
|
|
5
|
+
export function prepareApiRequest(options) {
|
|
6
|
+
const resolved = resolveConfig();
|
|
7
|
+
const config = resolved.config;
|
|
8
|
+
const baseUrl = (options.baseUrl || config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
9
|
+
const baseUrlSource = options.baseUrl ? 'option:base-url' : resolved.sources.baseUrl || 'default';
|
|
10
|
+
const apiKey = options.apiKey || config.apiKey;
|
|
11
|
+
const apiKeySource = options.apiKey ? 'option:api-key' : resolved.sources.apiKey;
|
|
12
|
+
assertTrustedCredentialTarget(baseUrl, apiKeySource, baseUrlSource);
|
|
13
|
+
const path = applyPathParams(options.path, options.pathParams || {});
|
|
14
|
+
const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
15
|
+
for (const [key, value] of Object.entries(options.query || {})) {
|
|
16
|
+
url.searchParams.set(key, value);
|
|
17
|
+
}
|
|
18
|
+
const headers = new Headers();
|
|
19
|
+
if (apiKey)
|
|
20
|
+
headers.set('Authorization', `Bearer ${apiKey}`);
|
|
21
|
+
for (const [key, value] of Object.entries(options.headers || {}))
|
|
22
|
+
headers.set(key, value);
|
|
23
|
+
const hasFiles = Boolean(options.files && Object.keys(options.files).length > 0);
|
|
24
|
+
const hasFormOnly = Boolean(!hasFiles && options.form && Object.keys(options.form).length > 0);
|
|
25
|
+
const hasJson = options.body !== undefined;
|
|
26
|
+
const hasRaw = Boolean(options.rawBodyFile);
|
|
27
|
+
const bodyModes = [hasFiles, hasFormOnly, hasJson, hasRaw].filter(Boolean).length;
|
|
28
|
+
if (bodyModes > 1)
|
|
29
|
+
throw new Error('Use only one body mode: --file/--form, --body-json/--body-file, or --raw-body-file.');
|
|
30
|
+
let body;
|
|
31
|
+
if (hasFiles) {
|
|
32
|
+
const formData = new FormData();
|
|
33
|
+
for (const [key, value] of Object.entries(options.form || {}))
|
|
34
|
+
formData.set(key, value);
|
|
35
|
+
for (const [key, filePath] of Object.entries(options.files || {})) {
|
|
36
|
+
const bytes = readFileSync(filePath);
|
|
37
|
+
formData.set(key, new Blob([bytes]), basename(filePath));
|
|
38
|
+
}
|
|
39
|
+
body = formData;
|
|
40
|
+
}
|
|
41
|
+
else if (hasFormOnly) {
|
|
42
|
+
const params = new URLSearchParams();
|
|
43
|
+
for (const [key, value] of Object.entries(options.form || {}))
|
|
44
|
+
params.set(key, value);
|
|
45
|
+
setContentType(headers, options.contentType || 'application/x-www-form-urlencoded');
|
|
46
|
+
body = params;
|
|
47
|
+
}
|
|
48
|
+
else if (hasJson) {
|
|
49
|
+
setContentType(headers, options.contentType || 'application/json');
|
|
50
|
+
body = JSON.stringify(options.body);
|
|
51
|
+
}
|
|
52
|
+
else if (hasRaw && options.rawBodyFile) {
|
|
53
|
+
const bytes = readFileSync(options.rawBodyFile);
|
|
54
|
+
setContentType(headers, options.contentType || 'application/octet-stream');
|
|
55
|
+
body = new Blob([bytes]);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
url: url.toString(),
|
|
59
|
+
init: {
|
|
60
|
+
method: options.method,
|
|
61
|
+
headers,
|
|
62
|
+
body,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export async function runApiRequest(options) {
|
|
67
|
+
const request = prepareApiRequest(options);
|
|
68
|
+
const response = await fetch(request.url, request.init);
|
|
69
|
+
const contentType = response.headers.get('content-type') || '';
|
|
70
|
+
const payload = contentType.includes('application/json')
|
|
71
|
+
? await response.json().catch(() => null)
|
|
72
|
+
: await response.text();
|
|
73
|
+
return {
|
|
74
|
+
ok: response.ok,
|
|
75
|
+
status: response.status,
|
|
76
|
+
statusText: response.statusText,
|
|
77
|
+
rateLimit: {
|
|
78
|
+
limit: response.headers.get('x-ratelimit-limit'),
|
|
79
|
+
remaining: response.headers.get('x-ratelimit-remaining'),
|
|
80
|
+
reset: response.headers.get('x-ratelimit-reset'),
|
|
81
|
+
retryAfter: response.headers.get('retry-after'),
|
|
82
|
+
},
|
|
83
|
+
data: payload,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function applyPathParams(path, params) {
|
|
87
|
+
return path.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
88
|
+
const value = params[key];
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
throw new Error(`Missing path param "${key}". Pass --path ${key}=...`);
|
|
91
|
+
return encodeURIComponent(value);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function setContentType(headers, contentType) {
|
|
95
|
+
if (!headers.has('content-type'))
|
|
96
|
+
headers.set('Content-Type', contentType);
|
|
97
|
+
}
|
|
98
|
+
function assertTrustedCredentialTarget(baseUrl, apiKeySource, baseUrlSource) {
|
|
99
|
+
if (apiKeySource !== 'config-file')
|
|
100
|
+
return;
|
|
101
|
+
if (isTrustedZernioUrl(baseUrl))
|
|
102
|
+
return;
|
|
103
|
+
if (baseUrlSource === 'config-file')
|
|
104
|
+
return;
|
|
105
|
+
throw new Error('Refusing to send a saved API key to a custom API URL. Pass --api-key with --base-url, or save both values with zernio auth:set.');
|
|
106
|
+
}
|
|
107
|
+
function isTrustedZernioUrl(value) {
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL(value);
|
|
110
|
+
return url.protocol === 'https:' && (url.hostname === 'zernio.com' || url.hostname.endsWith('.zernio.com'));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
export function toArray(value) {
|
|
3
|
+
if (value === undefined || value === null)
|
|
4
|
+
return [];
|
|
5
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
6
|
+
}
|
|
7
|
+
export function parseKeyValueList(items) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const item of toArray(items)) {
|
|
10
|
+
const index = item.indexOf('=');
|
|
11
|
+
if (index === -1)
|
|
12
|
+
throw new Error(`Expected key=value, got "${item}"`);
|
|
13
|
+
const key = item.slice(0, index).trim();
|
|
14
|
+
if (!key)
|
|
15
|
+
throw new Error(`Expected non-empty key in "${item}"`);
|
|
16
|
+
result[key] = item.slice(index + 1);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export function parseJsonInput(json, file) {
|
|
21
|
+
if (json && file)
|
|
22
|
+
throw new Error('Use either --body-json or --body-file, not both.');
|
|
23
|
+
if (!json && !file)
|
|
24
|
+
return undefined;
|
|
25
|
+
const raw = file ? readFileSync(file, 'utf8') : json;
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config stored at ~/.zernio/config.json.
|
|
3
|
+
* Env vars ZERNIO_API_KEY and ZERNIO_API_URL take precedence over file values.
|
|
4
|
+
* Legacy env vars LATE_API_KEY / LATE_API_URL and ~/.late/config.json are still
|
|
5
|
+
* supported as fallbacks for backwards compatibility.
|
|
6
|
+
*/
|
|
7
|
+
export interface ZernioConfig {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ResolvedConfig {
|
|
12
|
+
config: ZernioConfig;
|
|
13
|
+
sources: {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Write config to disk, merging with existing values. */
|
|
19
|
+
export declare function writeConfig(updates: Partial<ZernioConfig>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Get resolved config. Env vars override file values.
|
|
22
|
+
* Priority: ZERNIO_* env var > LATE_* env var (legacy) > config file > default.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getConfig(): ZernioConfig;
|
|
25
|
+
export declare function resolveConfig(): ResolvedConfig;
|
|
26
|
+
/** Get API key or exit with error. */
|
|
27
|
+
export declare function requireApiKey(): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { config as loadDotenv } from 'dotenv';
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.zernio');
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
+
/** Legacy config path for backwards compatibility */
|
|
8
|
+
const LEGACY_CONFIG_FILE = join(homedir(), '.late', 'config.json');
|
|
9
|
+
let dotenvLoaded = false;
|
|
10
|
+
function loadDotenvFiles() {
|
|
11
|
+
if (dotenvLoaded)
|
|
12
|
+
return;
|
|
13
|
+
dotenvLoaded = true;
|
|
14
|
+
if (process.env.ZERNIO_CLI_LOAD_ENV !== '1')
|
|
15
|
+
return;
|
|
16
|
+
const fileNames = process.env.ZERNIO_CLI_ENV_FILE
|
|
17
|
+
? [process.env.ZERNIO_CLI_ENV_FILE]
|
|
18
|
+
: ['.env.local', `.env.${process.env.NODE_ENV || 'development'}`, '.env'];
|
|
19
|
+
for (const fileName of fileNames) {
|
|
20
|
+
if (existsSync(fileName))
|
|
21
|
+
loadDotenv({ path: fileName, override: false, quiet: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Read config from a JSON file. Returns empty object if file doesn't exist. */
|
|
25
|
+
function readJsonConfig(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(filePath))
|
|
28
|
+
return {};
|
|
29
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read config from disk. Checks ~/.zernio/config.json first,
|
|
38
|
+
* falls back to ~/.late/config.json for backwards compatibility.
|
|
39
|
+
*/
|
|
40
|
+
function readConfigFile() {
|
|
41
|
+
const config = readJsonConfig(CONFIG_FILE);
|
|
42
|
+
if (config.apiKey)
|
|
43
|
+
return config;
|
|
44
|
+
// Fallback to legacy config
|
|
45
|
+
return readJsonConfig(LEGACY_CONFIG_FILE);
|
|
46
|
+
}
|
|
47
|
+
/** Write config to disk, merging with existing values. */
|
|
48
|
+
export function writeConfig(updates) {
|
|
49
|
+
const existing = readJsonConfig(CONFIG_FILE);
|
|
50
|
+
const merged = { ...existing, ...updates };
|
|
51
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
52
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
chmodSync(CONFIG_DIR, 0o700);
|
|
56
|
+
}
|
|
57
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
|
|
58
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get resolved config. Env vars override file values.
|
|
62
|
+
* Priority: ZERNIO_* env var > LATE_* env var (legacy) > config file > default.
|
|
63
|
+
*/
|
|
64
|
+
export function getConfig() {
|
|
65
|
+
return resolveConfig().config;
|
|
66
|
+
}
|
|
67
|
+
export function resolveConfig() {
|
|
68
|
+
loadDotenvFiles();
|
|
69
|
+
const file = readConfigFile();
|
|
70
|
+
const apiKey = process.env.ZERNIO_API_KEY ||
|
|
71
|
+
process.env.LATE_API_KEY ||
|
|
72
|
+
file.apiKey;
|
|
73
|
+
const baseUrl = process.env.ZERNIO_API_URL ||
|
|
74
|
+
process.env.LATE_API_URL ||
|
|
75
|
+
file.baseUrl;
|
|
76
|
+
return {
|
|
77
|
+
config: {
|
|
78
|
+
apiKey,
|
|
79
|
+
baseUrl,
|
|
80
|
+
},
|
|
81
|
+
sources: {
|
|
82
|
+
apiKey: process.env.ZERNIO_API_KEY
|
|
83
|
+
? 'env:ZERNIO_API_KEY'
|
|
84
|
+
: process.env.LATE_API_KEY
|
|
85
|
+
? 'env:LATE_API_KEY'
|
|
86
|
+
: file.apiKey
|
|
87
|
+
? 'config-file'
|
|
88
|
+
: undefined,
|
|
89
|
+
baseUrl: process.env.ZERNIO_API_URL
|
|
90
|
+
? 'env:ZERNIO_API_URL'
|
|
91
|
+
: process.env.LATE_API_URL
|
|
92
|
+
? 'env:LATE_API_URL'
|
|
93
|
+
: file.baseUrl
|
|
94
|
+
? 'config-file'
|
|
95
|
+
: undefined,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Get API key or exit with error. */
|
|
100
|
+
export function requireApiKey() {
|
|
101
|
+
const { apiKey } = getConfig();
|
|
102
|
+
if (!apiKey) {
|
|
103
|
+
console.error(JSON.stringify({
|
|
104
|
+
ok: false,
|
|
105
|
+
error: true,
|
|
106
|
+
message: 'No API key configured. Run "zernio auth:set" or set ZERNIO_API_KEY env var.',
|
|
107
|
+
}));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
return apiKey;
|
|
111
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LateApiError } from '@zernio/node';
|
|
2
|
+
import { outputError } from './output.js';
|
|
3
|
+
/**
|
|
4
|
+
* Handle errors from SDK calls.
|
|
5
|
+
* LateApiError instances get structured JSON output; everything else gets a generic message.
|
|
6
|
+
*/
|
|
7
|
+
export function handleError(err) {
|
|
8
|
+
if (err instanceof LateApiError) {
|
|
9
|
+
outputError(err.message, err.statusCode);
|
|
10
|
+
}
|
|
11
|
+
outputError(err.message);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OpenApiEndpoint } from '../generated/openapi-catalog.js';
|
|
2
|
+
export declare function listEndpoints(filters: {
|
|
3
|
+
tag?: string;
|
|
4
|
+
method?: string;
|
|
5
|
+
search?: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
}): OpenApiEndpoint[];
|
|
8
|
+
export declare function findEndpoint(input: string): OpenApiEndpoint | undefined;
|
|
9
|
+
export declare function endpointSuggestions(input: string): OpenApiEndpoint[];
|
|
10
|
+
export declare function endpointToSummary(endpoint: OpenApiEndpoint): {
|
|
11
|
+
operationId: "listAccountGroups" | "createAccountGroup" | "deleteAccountGroup" | "updateAccountGroup" | "deleteInstagramIceBreakers" | "getInstagramIceBreakers" | "setInstagramIceBreakers" | "deleteMessengerMenu" | "getMessengerMenu" | "setMessengerMenu" | "deleteTelegramCommands" | "getTelegramCommands" | "setTelegramCommands" | "listAccounts" | "deleteAccount" | "moveAccountToProfile" | "updateAccount" | "getAccountHealth" | "getTikTokCreatorInfo" | "getFollowerStats" | "getAllAccountsHealth" | "listAdAudiences" | "createAdAudience" | "deleteAdAudience" | "getAdAudience" | "addUsersToAdAudience" | "updateAdSet" | "updateAdSetStatus" | "listAdCampaigns" | "deleteAdCampaign" | "updateAdCampaign" | "duplicateAdCampaign" | "updateAdCampaignStatus" | "bulkUpdateAdCampaignStatus" | "getAdsTimeline" | "getAdTree" | "listConversionDestinations" | "createConversionDestination" | "deleteConversionDestination" | "getConversionDestination" | "updateConversionDestination" | "removeConversionAssociations" | "listConversionAssociations" | "addConversionAssociations" | "getConversionMetrics" | "listAds" | "deleteAd" | "getAd" | "updateAd" | "getAdAnalytics" | "getAdComments" | "getAdTrackingTags" | "updateAdTrackingTags" | "listAdAccounts" | "boostPost" | "listAdsBusinessCenters" | "listAdCatalogs" | "listAdCatalogProductSets" | "sendConversions" | "adjustConversions" | "getConversionsQuality" | "createStandaloneAd" | "createCtwaAd" | "searchAdInterests" | "listLeadForms" | "createLeadForm" | "archiveLeadForm" | "getLeadForm" | "listFormLeads" | "createTestLead" | "listLeads" | "estimateAdReach" | "searchAdTargeting" | "getLinkedInAggregateAnalytics" | "getLinkedInPostAnalytics" | "getLinkedInPostReactions" | "getAnalytics" | "getBestTimeToPost" | "getContentDecay" | "getDailyMetrics" | "getFacebookPageInsights" | "getGoogleBusinessPerformance" | "getGoogleBusinessSearchKeywords" | "getInstagramAccountInsights" | "getInstagramDemographics" | "getInstagramFollowerHistory" | "getLinkedInOrgAggregateAnalytics" | "getPostTimeline" | "getPostingFrequency" | "getTikTokAccountInsights" | "getYouTubeChannelInsights" | "getYouTubeDailyViews" | "getYouTubeDemographics" | "getYouTubeVideoRetention" | "listApiKeys" | "createApiKey" | "deleteApiKey" | "listBroadcasts" | "createBroadcast" | "deleteBroadcast" | "getBroadcast" | "updateBroadcast" | "cancelBroadcast" | "listBroadcastRecipients" | "addBroadcastRecipients" | "scheduleBroadcast" | "sendBroadcast" | "listCommentAutomations" | "createCommentAutomation" | "deleteCommentAutomation" | "getCommentAutomation" | "updateCommentAutomation" | "listCommentAutomationLogs" | "listInboxComments" | "deleteInboxComment" | "getInboxPostComments" | "replyToInboxPost" | "unhideInboxComment" | "hideInboxComment" | "unlikeInboxComment" | "likeInboxComment" | "sendPrivateReplyToComment" | "getFacebookPages" | "updateFacebookPage" | "getGmbLocations" | "updateGmbLocation" | "updateLinkedInOrganization" | "getLinkedInOrganizations" | "getPinterestBoards" | "updatePinterestBoards" | "getRedditFlairs" | "getRedditSubreddits" | "updateRedditSubreddits" | "getYoutubePlaylists" | "updateYoutubeDefaultPlaylist" | "getConnectUrl" | "handleOAuthCallback" | "connectAds" | "connectBlueskyCredentials" | "listFacebookPages" | "selectFacebookPage" | "listGoogleBusinessLocations" | "selectGoogleBusinessLocation" | "listLinkedInOrganizations" | "selectLinkedInOrganization" | "getPendingOAuthData" | "listPinterestBoardsForSelection" | "selectPinterestBoard" | "listSnapchatProfiles" | "selectSnapchatProfile" | "getTelegramConnectStatus" | "completeTelegramConnect" | "initiateTelegramConnect" | "configureTikTokAdsBrandIdentity" | "connectWhatsAppCredentials" | "listWhatsAppPhoneNumbers" | "completeWhatsAppPhoneSelection" | "listContacts" | "createContact" | "deleteContact" | "getContact" | "updateContact" | "getContactChannels" | "bulkCreateContacts" | "clearContactFieldValue" | "setContactFieldValue" | "listCustomFields" | "createCustomField" | "deleteCustomField" | "updateCustomField" | "getDiscordChannels" | "getDiscordSettings" | "updateDiscordSettings" | "listDiscordPinnedMessages" | "unpinDiscordMessage" | "pinDiscordMessage" | "sendDiscordDirectMessage" | "listDiscordScheduledEvents" | "createDiscordScheduledEvent" | "deleteDiscordScheduledEvent" | "getDiscordScheduledEvent" | "updateDiscordScheduledEvent" | "listDiscordGuildMembers" | "removeDiscordMemberRole" | "addDiscordMemberRole" | "listDiscordGuildRoles" | "getGmbAttributeMetadata" | "getGoogleBusinessAttributes" | "updateGoogleBusinessAttributes" | "getGoogleBusinessFoodMenus" | "updateGoogleBusinessFoodMenus" | "getGoogleBusinessLocationDetails" | "updateGoogleBusinessLocationDetails" | "deleteGoogleBusinessMedia" | "listGoogleBusinessMedia" | "createGoogleBusinessMedia" | "deleteGoogleBusinessPlaceAction" | "listGoogleBusinessPlaceActions" | "updateGoogleBusinessPlaceAction" | "createGoogleBusinessPlaceAction" | "getGoogleBusinessReviews" | "deleteGoogleBusinessReviewReply" | "replyToGoogleBusinessReview" | "batchGetGoogleBusinessReviews" | "getGoogleBusinessServices" | "updateGoogleBusinessServices" | "getGoogleBusinessVerifications" | "startGoogleBusinessVerification" | "completeGoogleBusinessVerification" | "fetchGoogleBusinessVerificationOptions" | "listInboxConversationAnalytics" | "getInboxConversationAnalytics" | "getInboxHeatmap" | "getInboxResponseTime" | "getInboxSourceBreakdown" | "getInboxTopAccounts" | "getInboxVolume" | "listInstagramStories" | "getInstagramStoryInsights" | "createInviteToken" | "getLinkedInMentions" | "listLogs" | "getMediaPresignedUrl" | "listInboxConversations" | "createInboxConversation" | "getInboxConversation" | "updateInboxConversation" | "getInboxConversationMessages" | "sendInboxMessage" | "deleteInboxMessage" | "editInboxMessage" | "removeMessageReaction" | "addMessageReaction" | "markConversationRead" | "sendTypingIndicator" | "uploadMediaDirect" | "listPosts" | "createPost" | "deletePost" | "getPost" | "updatePost" | "editPost" | "retryPost" | "unpublishPost" | "updatePostMetadata" | "bulkUploadPosts" | "listProfiles" | "createProfile" | "deleteProfile" | "getProfile" | "updateProfile" | "getNextQueueSlot" | "previewQueue" | "deleteQueueSlot" | "listQueueSlots" | "createQueueSlot" | "updateQueueSlot" | "getRedditFeed" | "searchReddit" | "listInboxReviews" | "deleteInboxReviewReply" | "replyToInboxReview" | "listSequences" | "createSequence" | "deleteSequence" | "getSequence" | "updateSequence" | "activateSequence" | "enrollContacts" | "unenrollContact" | "listSequenceEnrollments" | "pauseSequence" | "listTrackingTags" | "createTrackingTag" | "getTrackingTag" | "updateTrackingTag" | "removeTrackingTagSharedAccount" | "listTrackingTagSharedAccounts" | "addTrackingTagSharedAccount" | "getTrackingTagStats" | "removeBookmark" | "bookmarkPost" | "unfollowUser" | "followUser" | "undoRetweet" | "retweetPost" | "getXApiPricing" | "getUsageStats" | "listUsers" | "getUser" | "validateMedia" | "validatePostLength" | "validatePost" | "validateSubreddit" | "getWebhookLogs" | "deleteWebhookSettings" | "getWebhookSettings" | "createWebhookSettings" | "updateWebhookSettings" | "testWebhook" | "getWhatsAppCallPermissions" | "getWhatsAppCallingConfig" | "listWhatsAppCalls" | "initiateWhatsAppCall" | "getWhatsAppCall" | "getWhatsAppCallEstimate" | "disableWhatsAppCalling" | "updateWhatsAppCalling" | "enableWhatsAppCalling" | "listWhatsAppFlowResponses" | "listWhatsAppFlows" | "createWhatsAppFlow" | "deleteWhatsAppFlow" | "getWhatsAppFlow" | "updateWhatsAppFlow" | "deprecateWhatsAppFlow" | "getWhatsAppFlowJson" | "uploadWhatsAppFlowJson" | "getWhatsAppFlowPreview" | "publishWhatsAppFlow" | "listWhatsAppFlowVersions" | "sendWhatsAppFlowMessage" | "getWhatsAppNumberInfo" | "getWhatsAppPhoneNumbers" | "getWhatsAppNumberRemediation" | "remediateWhatsAppNumber" | "releaseWhatsAppPhoneNumber" | "getWhatsAppPhoneNumber" | "checkWhatsAppNumberAvailability" | "searchAvailableWhatsAppNumbers" | "listWhatsAppNumberCountries" | "getWhatsAppNumberKycForm" | "submitWhatsAppNumberKyc" | "uploadWhatsAppNumberKycDocument" | "validateWhatsAppNumberKycAddress" | "purchaseWhatsAppPhoneNumber" | "listWhatsAppSandboxSessions" | "createWhatsAppSandboxSession" | "deleteWhatsAppSandboxSession" | "getWhatsAppLibraryTemplate" | "unblockWhatsAppUsers" | "getWhatsAppBlockedUsers" | "blockWhatsAppUsers" | "getWhatsAppBlockStatus" | "getWhatsAppBusinessProfile" | "updateWhatsAppBusinessProfile" | "getWhatsAppDisplayName" | "updateWhatsAppDisplayName" | "uploadWhatsAppProfilePhoto" | "listWhatsAppConversions" | "sendWhatsAppConversion" | "getWhatsAppDataset" | "createWhatsAppDataset" | "getWhatsAppTemplates" | "createWhatsAppTemplate" | "deleteWhatsAppTemplate" | "getWhatsAppTemplate" | "updateWhatsAppTemplate" | "listWhatsAppGroupChats" | "createWhatsAppGroupChat" | "deleteWhatsAppGroupChat" | "getWhatsAppGroupChat" | "updateWhatsAppGroupChat" | "createWhatsAppGroupInviteLink" | "rejectWhatsAppGroupJoinRequests" | "listWhatsAppGroupJoinRequests" | "approveWhatsAppGroupJoinRequests" | "removeWhatsAppGroupParticipants" | "addWhatsAppGroupParticipants" | "listWorkflows" | "createWorkflow" | "deleteWorkflow" | "getWorkflow" | "updateWorkflow" | "activateWorkflow" | "duplicateWorkflow" | "listWorkflowExecutions" | "triggerWorkflow" | "listWorkflowExecutionEvents" | "pauseWorkflow" | "listWorkflowVersions" | "getWorkflowVersion" | "restoreWorkflowVersion";
|
|
12
|
+
method: "POST" | "PUT" | "GET" | "DELETE" | "PATCH";
|
|
13
|
+
path: "/v1/account-groups" | "/v1/account-groups/{groupId}" | "/v1/accounts/{accountId}/instagram-ice-breakers" | "/v1/accounts/{accountId}/messenger-menu" | "/v1/accounts/{accountId}/telegram-commands" | "/v1/accounts" | "/v1/accounts/{accountId}" | "/v1/accounts/{accountId}/health" | "/v1/accounts/{accountId}/tiktok/creator-info" | "/v1/accounts/follower-stats" | "/v1/accounts/health" | "/v1/ads/audiences" | "/v1/ads/audiences/{audienceId}" | "/v1/ads/audiences/{audienceId}/users" | "/v1/ads/ad-sets/{adSetId}" | "/v1/ads/ad-sets/{adSetId}/status" | "/v1/ads/campaigns" | "/v1/ads/campaigns/{campaignId}" | "/v1/ads/campaigns/{campaignId}/duplicate" | "/v1/ads/campaigns/{campaignId}/status" | "/v1/ads/campaigns/bulk-status" | "/v1/ads/timeline" | "/v1/ads/tree" | "/v1/accounts/{accountId}/conversion-destinations" | "/v1/accounts/{accountId}/conversion-destinations/{destinationId}" | "/v1/accounts/{accountId}/conversion-destinations/{destinationId}/associations" | "/v1/accounts/{accountId}/conversion-destinations/{destinationId}/metrics" | "/v1/ads" | "/v1/ads/{adId}" | "/v1/ads/{adId}/analytics" | "/v1/ads/{adId}/comments" | "/v1/ads/{adId}/tracking-tags" | "/v1/ads/accounts" | "/v1/ads/boost" | "/v1/ads/business-centers" | "/v1/ads/catalogs" | "/v1/ads/catalogs/{catalogId}/product-sets" | "/v1/ads/conversions" | "/v1/ads/conversions/adjustments" | "/v1/ads/conversions/quality" | "/v1/ads/create" | "/v1/ads/ctwa" | "/v1/ads/interests" | "/v1/ads/lead-forms" | "/v1/ads/lead-forms/{formId}" | "/v1/ads/lead-forms/{formId}/leads" | "/v1/ads/lead-forms/{formId}/test-leads" | "/v1/ads/leads" | "/v1/ads/targeting/reach-estimate" | "/v1/ads/targeting/search" | "/v1/accounts/{accountId}/linkedin-aggregate-analytics" | "/v1/accounts/{accountId}/linkedin-post-analytics" | "/v1/accounts/{accountId}/linkedin-post-reactions" | "/v1/analytics" | "/v1/analytics/best-time" | "/v1/analytics/content-decay" | "/v1/analytics/daily-metrics" | "/v1/analytics/facebook/page-insights" | "/v1/analytics/googlebusiness/performance" | "/v1/analytics/googlebusiness/search-keywords" | "/v1/analytics/instagram/account-insights" | "/v1/analytics/instagram/demographics" | "/v1/analytics/instagram/follower-history" | "/v1/analytics/linkedin/org-aggregate-analytics" | "/v1/analytics/post-timeline" | "/v1/analytics/posting-frequency" | "/v1/analytics/tiktok/account-insights" | "/v1/analytics/youtube/channel-insights" | "/v1/analytics/youtube/daily-views" | "/v1/analytics/youtube/demographics" | "/v1/analytics/youtube/video-retention" | "/v1/api-keys" | "/v1/api-keys/{keyId}" | "/v1/broadcasts" | "/v1/broadcasts/{broadcastId}" | "/v1/broadcasts/{broadcastId}/cancel" | "/v1/broadcasts/{broadcastId}/recipients" | "/v1/broadcasts/{broadcastId}/schedule" | "/v1/broadcasts/{broadcastId}/send" | "/v1/comment-automations" | "/v1/comment-automations/{automationId}" | "/v1/comment-automations/{automationId}/logs" | "/v1/inbox/comments" | "/v1/inbox/comments/{postId}" | "/v1/inbox/comments/{postId}/{commentId}/hide" | "/v1/inbox/comments/{postId}/{commentId}/like" | "/v1/inbox/comments/{postId}/{commentId}/private-reply" | "/v1/accounts/{accountId}/facebook-page" | "/v1/accounts/{accountId}/gmb-locations" | "/v1/accounts/{accountId}/linkedin-organization" | "/v1/accounts/{accountId}/linkedin-organizations" | "/v1/accounts/{accountId}/pinterest-boards" | "/v1/accounts/{accountId}/reddit-flairs" | "/v1/accounts/{accountId}/reddit-subreddits" | "/v1/accounts/{accountId}/youtube-playlists" | "/v1/connect/{platform}" | "/v1/connect/{platform}/ads" | "/v1/connect/bluesky/credentials" | "/v1/connect/facebook/select-page" | "/v1/connect/googlebusiness/locations" | "/v1/connect/googlebusiness/select-location" | "/v1/connect/linkedin/organizations" | "/v1/connect/linkedin/select-organization" | "/v1/connect/pending-data" | "/v1/connect/pinterest/select-board" | "/v1/connect/snapchat/select-profile" | "/v1/connect/telegram" | "/v1/connect/tiktok-ads" | "/v1/connect/whatsapp/credentials" | "/v1/connect/whatsapp/select-phone-number" | "/v1/contacts" | "/v1/contacts/{contactId}" | "/v1/contacts/{contactId}/channels" | "/v1/contacts/bulk" | "/v1/contacts/{contactId}/fields/{slug}" | "/v1/custom-fields" | "/v1/custom-fields/{fieldId}" | "/v1/accounts/{accountId}/discord-channels" | "/v1/accounts/{accountId}/discord-settings" | "/v1/discord/channels/{channelId}/pins" | "/v1/discord/channels/{channelId}/pins/{messageId}" | "/v1/discord/dms" | "/v1/discord/guilds/{guildId}/events" | "/v1/discord/guilds/{guildId}/events/{eventId}" | "/v1/discord/guilds/{guildId}/members" | "/v1/discord/guilds/{guildId}/members/{userId}/roles/{roleId}" | "/v1/discord/guilds/{guildId}/roles" | "/v1/accounts/{accountId}/gmb-attribute-metadata" | "/v1/accounts/{accountId}/gmb-attributes" | "/v1/accounts/{accountId}/gmb-food-menus" | "/v1/accounts/{accountId}/gmb-location-details" | "/v1/accounts/{accountId}/gmb-media" | "/v1/accounts/{accountId}/gmb-place-actions" | "/v1/accounts/{accountId}/gmb-reviews" | "/v1/accounts/{accountId}/gmb-reviews/{reviewId}/reply" | "/v1/accounts/{accountId}/gmb-reviews/batch" | "/v1/accounts/{accountId}/gmb-services" | "/v1/accounts/{accountId}/gmb-verifications" | "/v1/accounts/{accountId}/gmb-verifications/{verificationId}/complete" | "/v1/accounts/{accountId}/gmb-verifications/options" | "/v1/analytics/inbox/conversations" | "/v1/analytics/inbox/conversations/{conversationId}" | "/v1/analytics/inbox/heatmap" | "/v1/analytics/inbox/response-time" | "/v1/analytics/inbox/source-breakdown" | "/v1/analytics/inbox/top-accounts" | "/v1/analytics/inbox/volume" | "/v1/accounts/{accountId}/instagram/stories" | "/v1/accounts/{accountId}/instagram/stories/{storyId}/insights" | "/v1/invite/tokens" | "/v1/accounts/{accountId}/linkedin-mentions" | "/v1/logs" | "/v1/media/presign" | "/v1/inbox/conversations" | "/v1/inbox/conversations/{conversationId}" | "/v1/inbox/conversations/{conversationId}/messages" | "/v1/inbox/conversations/{conversationId}/messages/{messageId}" | "/v1/inbox/conversations/{conversationId}/messages/{messageId}/reactions" | "/v1/inbox/conversations/{conversationId}/read" | "/v1/inbox/conversations/{conversationId}/typing" | "/v1/media/upload-direct" | "/v1/posts" | "/v1/posts/{postId}" | "/v1/posts/{postId}/edit" | "/v1/posts/{postId}/retry" | "/v1/posts/{postId}/unpublish" | "/v1/posts/{postId}/update-metadata" | "/v1/posts/bulk-upload" | "/v1/profiles" | "/v1/profiles/{profileId}" | "/v1/queue/next-slot" | "/v1/queue/preview" | "/v1/queue/slots" | "/v1/reddit/feed" | "/v1/reddit/search" | "/v1/inbox/reviews" | "/v1/inbox/reviews/{reviewId}/reply" | "/v1/sequences" | "/v1/sequences/{sequenceId}" | "/v1/sequences/{sequenceId}/activate" | "/v1/sequences/{sequenceId}/enroll" | "/v1/sequences/{sequenceId}/enroll/{contactId}" | "/v1/sequences/{sequenceId}/enrollments" | "/v1/sequences/{sequenceId}/pause" | "/v1/accounts/{accountId}/tracking-tags" | "/v1/accounts/{accountId}/tracking-tags/{tagId}" | "/v1/accounts/{accountId}/tracking-tags/{tagId}/shared-accounts" | "/v1/accounts/{accountId}/tracking-tags/{tagId}/stats" | "/v1/twitter/bookmark" | "/v1/twitter/follow" | "/v1/twitter/retweet" | "/v1/billing/x-pricing" | "/v1/usage-stats" | "/v1/users" | "/v1/users/{userId}" | "/v1/tools/validate/media" | "/v1/tools/validate/post-length" | "/v1/tools/validate/post" | "/v1/tools/validate/subreddit" | "/v1/webhooks/logs" | "/v1/webhooks/settings" | "/v1/webhooks/test" | "/v1/whatsapp/call-permissions" | "/v1/whatsapp/calling" | "/v1/whatsapp/calls" | "/v1/whatsapp/calls/{callId}" | "/v1/whatsapp/calls/estimate" | "/v1/whatsapp/phone-numbers/{id}/calling" | "/v1/whatsapp/flow-responses" | "/v1/whatsapp/flows" | "/v1/whatsapp/flows/{flowId}" | "/v1/whatsapp/flows/{flowId}/deprecate" | "/v1/whatsapp/flows/{flowId}/json" | "/v1/whatsapp/flows/{flowId}/preview" | "/v1/whatsapp/flows/{flowId}/publish" | "/v1/whatsapp/flows/{flowId}/versions" | "/v1/whatsapp/flows/send" | "/v1/whatsapp/number-info" | "/v1/whatsapp/phone-numbers" | "/v1/whatsapp/phone-numbers/{id}/remediate" | "/v1/whatsapp/phone-numbers/{phoneNumberId}" | "/v1/whatsapp/phone-numbers/availability" | "/v1/whatsapp/phone-numbers/available" | "/v1/whatsapp/phone-numbers/countries" | "/v1/whatsapp/phone-numbers/kyc" | "/v1/whatsapp/phone-numbers/kyc/upload-document" | "/v1/whatsapp/phone-numbers/kyc/validate-address" | "/v1/whatsapp/phone-numbers/purchase" | "/v1/whatsapp/sandbox/sessions" | "/v1/whatsapp/sandbox/sessions/{sessionId}" | "/v1/whatsapp/template-library" | "/v1/whatsapp/block-users" | "/v1/whatsapp/block-users/status" | "/v1/whatsapp/business-profile" | "/v1/whatsapp/business-profile/display-name" | "/v1/whatsapp/business-profile/photo" | "/v1/whatsapp/conversions" | "/v1/whatsapp/dataset" | "/v1/whatsapp/templates" | "/v1/whatsapp/templates/{templateName}" | "/v1/whatsapp/wa-groups" | "/v1/whatsapp/wa-groups/{groupId}" | "/v1/whatsapp/wa-groups/{groupId}/invite-link" | "/v1/whatsapp/wa-groups/{groupId}/join-requests" | "/v1/whatsapp/wa-groups/{groupId}/participants" | "/v1/workflows" | "/v1/workflows/{workflowId}" | "/v1/workflows/{workflowId}/activate" | "/v1/workflows/{workflowId}/duplicate" | "/v1/workflows/{workflowId}/executions" | "/v1/workflows/{workflowId}/executions/{executionId}/events" | "/v1/workflows/{workflowId}/pause" | "/v1/workflows/{workflowId}/versions" | "/v1/workflows/{workflowId}/versions/{version}" | "/v1/workflows/{workflowId}/versions/{version}/restore";
|
|
14
|
+
tags: readonly ["Account Groups"] | readonly ["Account Settings"] | readonly ["Accounts"] | readonly ["Accounts", "Analytics"] | readonly ["Ad Audiences"] | readonly ["Ad Campaigns"] | readonly ["Ads"] | readonly ["Analytics"] | readonly ["API Keys"] | readonly ["Broadcasts"] | readonly ["Comment Automations"] | readonly ["Comments"] | readonly ["Connect"] | readonly ["Contacts"] | readonly ["Custom Fields"] | readonly ["Discord"] | readonly ["GMB Attributes"] | readonly ["GMB Food Menus"] | readonly ["GMB Location Details"] | readonly ["GMB Media"] | readonly ["GMB Place Actions"] | readonly ["GMB Reviews"] | readonly ["GMB Services"] | readonly ["GMB Verifications"] | readonly ["Inbox Analytics"] | readonly ["Instagram"] | readonly ["Invites"] | readonly ["LinkedIn Mentions"] | readonly ["Logs"] | readonly ["Media"] | readonly ["Messages"] | readonly ["Posts"] | readonly ["Profiles"] | readonly ["Queue"] | readonly ["Reddit Search"] | readonly ["Reviews"] | readonly ["Sequences"] | readonly ["Tracking Tags"] | readonly ["Twitter Engagement"] | readonly ["Usage"] | readonly ["Users"] | readonly ["Validate"] | readonly ["Webhooks"] | readonly ["WhatsApp Calling"] | readonly ["WhatsApp Flows"] | readonly ["WhatsApp Phone Numbers"] | readonly ["WhatsApp Sandbox"] | readonly ["WhatsApp Templates"] | readonly ["WhatsApp"] | readonly ["WhatsApp", "Ads"] | readonly ["Workflows"];
|
|
15
|
+
summary: "List posts" | "Get post analytics" | "Reply to a review" | "List contacts" | "List channels for a contact" | "List broadcasts" | "List broadcast recipients" | "Add recipients to a broadcast" | "List sequences" | "List enrollments for a sequence" | "List comment-to-DM automations" | "List groups" | "Create group" | "Delete group" | "Update group" | "Delete IG ice breakers" | "Get IG ice breakers" | "Set IG ice breakers" | "Delete FB persistent menu" | "Get FB persistent menu" | "Set FB persistent menu" | "Delete TG bot commands" | "Get TG bot commands" | "Set TG bot commands" | "List accounts" | "Disconnect account" | "Move account to a different profile" | "Update account" | "Check account health" | "Get TikTok creator info" | "Get follower stats" | "Check accounts health" | "List custom audiences" | "Create custom audience" | "Delete custom audience" | "Get audience details" | "Add users to audience" | "Update an ad set (budget, status, and/or bid strategy)" | "Pause or resume a single ad set" | "List campaigns" | "Delete a campaign" | "Update a campaign (budget and/or bid strategy)" | "Duplicate a campaign" | "Pause or resume a campaign" | "Pause or resume many campaigns" | "Get daily aggregate ad metrics for an account" | "Get campaign tree" | "List destinations for the Conversions API" | "Create a conversion destination (LinkedIn, Google Ads)" | "Soft-delete a conversion destination" | "Fetch a single conversion destination" | "Update a conversion destination" | "Remove campaign↔conversion associations" | "List campaigns associated with a conversion destination" | "Associate campaigns with a conversion destination" | "Fetch attribution metrics for a conversion destination" | "List ads" | "Cancel an ad" | "Get ad details" | "Update ad" | "Get ad analytics" | "List comments on an ad" | "Read an ad's click-URL tracking tags" | "Set/update an ad's click-URL tracking tags" | "List ad accounts" | "Boost post as ad" | "List TikTok Business Centers" | "List Meta product catalogs" | "List a catalog's product sets" | "Send conversion events to an ad platform" | "Adjust already-uploaded conversions (Google only)" | "Read Event Match Quality + coverage for a Meta pixel" | "Create standalone ad" | "Create Click-to-WhatsApp ad(s)" | "Search targeting interests (deprecated)" | "List Lead Gen (Instant) forms" | "Create a Lead Gen (Instant) form" | "Archive a Lead Gen form" | "Get a single Lead Gen form" | "List leads for a single form" | "Create a synthetic test lead" | "List submitted leads (cross-form CRM view)" | "Estimate audience reach" | "Search targeting options" | "Get LinkedIn aggregate stats" | "Get LinkedIn post stats" | "Get LinkedIn post reactions" | "Get best times to post" | "Get content performance decay" | "Get daily aggregated metrics" | "Get Facebook Page insights" | "Get GBP performance metrics" | "Get GBP search keywords" | "Get Instagram insights" | "Get Instagram demographics" | "Get Instagram follower history" | "Get LinkedIn organization page aggregate analytics" | "Get post analytics timeline" | "Get frequency vs engagement" | "Get TikTok account-level insights" | "Get YouTube channel-level insights" | "Get YouTube daily views" | "Get YouTube demographics" | "Get YouTube video retention curve" | "List keys" | "Create key" | "Delete key" | "Create broadcast draft" | "Delete broadcast" | "Get broadcast details" | "Update broadcast" | "Cancel broadcast" | "Schedule broadcast for later" | "Send broadcast now" | "Create comment-to-DM automation" | "Delete automation" | "Get automation details" | "Update automation settings" | "List automation logs" | "List commented posts" | "Delete comment" | "Get post comments" | "Reply to comment" | "Unhide comment" | "Hide comment" | "Unlike comment" | "Like comment" | "Send private reply" | "List Facebook pages" | "Update Facebook page" | "List GBP locations" | "Update GBP location" | "Switch LinkedIn account type" | "List LinkedIn orgs" | "List Pinterest boards" | "Set default Pinterest board" | "List subreddit flairs" | "List Reddit subreddits" | "Set default subreddit" | "List YouTube playlists" | "Set default YouTube playlist" | "Get OAuth connect URL" | "Complete OAuth callback" | "Connect ads for a platform" | "Connect Bluesky account" | "Select Facebook page" | "Select GBP location" | "Select LinkedIn org" | "Get pending OAuth data" | "Select Pinterest board" | "List Snapchat profiles" | "Select Snapchat profile" | "Generate Telegram code" | "Check Telegram status" | "Connect Telegram directly" | "Configure TikTok Ads Brand Identity" | "Connect WhatsApp via credentials" | "List WhatsApp phone numbers for selection" | "Complete WhatsApp phone number selection" | "Create contact" | "Delete contact" | "Get contact" | "Update contact" | "Bulk create contacts" | "Clear custom field value" | "Set custom field value" | "List custom field definitions" | "Create custom field" | "Delete custom field" | "Update custom field" | "List Discord guild channels" | "Get Discord account settings" | "Update Discord settings" | "List pinned messages in a Discord channel" | "Unpin a Discord message" | "Pin a Discord message" | "Send a Discord Direct Message" | "List Discord scheduled events" | "Create a Discord scheduled event" | "Delete a Discord scheduled event" | "Get a Discord scheduled event" | "Update a Discord scheduled event" | "List Discord guild members" | "Remove a role from a guild member" | "Assign a role to a guild member" | "List Discord guild roles" | "Get attribute metadata" | "Get attributes" | "Update attributes" | "Get food menus" | "Update food menus" | "Get location details" | "Update location details" | "Delete photo" | "List media" | "Upload photo" | "Delete action link" | "List action links" | "Update action link" | "Create action link" | "Get reviews" | "Delete a review reply" | "Batch get reviews" | "Get services" | "Replace services" | "Get verification state" | "Start a verification" | "Complete a verification" | "Fetch verification options" | "List conversations with inbox analytics" | "Get analytics for a single conversation" | "Get inbox day-of-week × hour-of-day heatmap" | "Get inbox response-time stats" | "Get inbox source breakdown" | "Get top accounts by inbox volume" | "Get inbox messaging volume" | "List active Instagram stories" | "Get Instagram story insights" | "Create invite token" | "Resolve LinkedIn mention" | "List activity logs" | "Get upload URL" | "List conversations" | "Create conversation (send a WhatsApp template)" | "Get conversation" | "Update conversation status" | "List messages" | "Send message" | "Delete message" | "Edit message" | "Remove reaction" | "Add reaction" | "Mark a conversation as read" | "Send typing indicator" | "Upload media file" | "Create post" | "Delete post" | "Get post" | "Update post" | "Edit published post" | "Retry failed post" | "Unpublish post" | "Update post metadata" | "Bulk upload from CSV" | "List profiles" | "Create profile" | "Delete profile" | "Get profile" | "Update profile" | "Get next available slot" | "Preview upcoming slots" | "Delete schedule" | "List schedules" | "Create schedule" | "Update schedule" | "Get subreddit feed" | "Search posts" | "List reviews" | "Delete review reply" | "Reply to review" | "Create sequence" | "Delete sequence" | "Get sequence with steps" | "Update sequence" | "Activate sequence" | "Enroll contacts in a sequence" | "Unenroll contact" | "Pause sequence" | "List tracking tags (Meta Pixels)" | "Create a tracking tag (Meta Pixel)" | "Fetch a single tracking tag (Meta Pixel)" | "Update a tracking tag (Meta Pixel)" | "Stop sharing a tracking tag with an ad account" | "List ad accounts a tracking tag is shared with" | "Share a tracking tag with an ad account" | "Aggregated event stats for a tracking tag (Meta Pixel)" | "Remove bookmark" | "Bookmark a tweet" | "Unfollow a user" | "Follow a user" | "Undo retweet" | "Retweet a post" | "Get X/Twitter API pricing table" | "Get plan and usage stats" | "List users" | "Get user" | "Validate media URL" | "Validate character count" | "Validate post content" | "Check subreddit existence" | "List webhook delivery logs" | "Delete webhook" | "List webhooks" | "Create webhook" | "Update webhook" | "Send test webhook" | "Check call permission for a consumer" | "Get calling config for an account" | "List call history for an account" | "Initiate outbound call" | "Get a single call" | "Estimate per-minute cost for a destination" | "Disable calling on a number" | "Update calling config" | "Enable calling on a number" | "List flow responses" | "List flows" | "Create flow" | "Delete flow" | "Get flow" | "Update flow" | "Deprecate flow" | "Get flow JSON asset" | "Upload flow JSON" | "Get flow preview URL" | "Publish flow" | "List flow versions" | "Send flow message" | "Get number status" | "List phone numbers" | "Get the declined requirements to fix" | "Fix a declined number and re-submit" | "Release phone number" | "Get phone number" | "Check a country's availability + address constraint" | "Search available numbers to purchase" | "List offerable number countries" | "Get regulated-number KYC form spec" | "Submit regulated-number KYC" | "Upload a single regulated-number KYC document" | "Pre-validate a regulated-number KYC address (Tier 4)" | "Purchase phone number" | "List your sandbox sessions" | "Start a sandbox activation for a phone" | "Revoke a sandbox session" | "Look up a library template" | "Unblock users" | "List blocked users" | "Block users" | "Check if a user is blocked" | "Get business profile" | "Update business profile" | "Get display name status" | "Request display name change" | "Upload profile picture" | "List recent WhatsApp conversion events" | "Send WhatsApp conversion event" | "Get CTWA conversions dataset" | "Provision CTWA conversions dataset" | "List templates" | "Create template" | "Delete template" | "Get template" | "Update template" | "List active groups" | "Get group info" | "Update group settings" | "Create invite link" | "Reject join requests" | "List join requests" | "Approve join requests" | "Remove participants" | "Add participants" | "List workflows" | "Create workflow" | "Delete workflow" | "Get workflow with graph" | "Update workflow" | "Activate workflow" | "Duplicate a workflow" | "List workflow runs" | "Manually start a workflow run" | "Get an execution's timeline" | "Pause workflow" | "List a workflow's version history" | "Get a specific workflow version" | "Restore a previous workflow version";
|
|
16
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { openApiEndpoints } from '../generated/openapi-catalog.js';
|
|
2
|
+
export function listEndpoints(filters) {
|
|
3
|
+
const tag = filters.tag?.toLowerCase();
|
|
4
|
+
const method = filters.method?.toUpperCase();
|
|
5
|
+
const search = filters.search?.toLowerCase();
|
|
6
|
+
const limit = filters.limit ?? 50;
|
|
7
|
+
return openApiEndpoints
|
|
8
|
+
.filter((endpoint) => {
|
|
9
|
+
if (tag && !endpoint.tags.some((value) => value.toLowerCase() === tag))
|
|
10
|
+
return false;
|
|
11
|
+
if (method && endpoint.method !== method)
|
|
12
|
+
return false;
|
|
13
|
+
if (!search)
|
|
14
|
+
return true;
|
|
15
|
+
const haystack = [
|
|
16
|
+
endpoint.operationId,
|
|
17
|
+
endpoint.method,
|
|
18
|
+
endpoint.path,
|
|
19
|
+
endpoint.summary,
|
|
20
|
+
'description' in endpoint ? endpoint.description : undefined,
|
|
21
|
+
endpoint.tags.join(' '),
|
|
22
|
+
].join(' ').toLowerCase();
|
|
23
|
+
return haystack.includes(search);
|
|
24
|
+
})
|
|
25
|
+
.slice(0, limit);
|
|
26
|
+
}
|
|
27
|
+
export function findEndpoint(input) {
|
|
28
|
+
const normalized = input.trim();
|
|
29
|
+
const byId = openApiEndpoints.find((endpoint) => endpoint.operationId.toLowerCase() === normalized.toLowerCase());
|
|
30
|
+
if (byId)
|
|
31
|
+
return byId;
|
|
32
|
+
const match = normalized.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(.+)$/i);
|
|
33
|
+
if (!match)
|
|
34
|
+
return undefined;
|
|
35
|
+
const method = match[1].toUpperCase();
|
|
36
|
+
const path = match[2].trim();
|
|
37
|
+
return openApiEndpoints.find((endpoint) => endpoint.method === method && endpoint.path === path);
|
|
38
|
+
}
|
|
39
|
+
export function endpointSuggestions(input) {
|
|
40
|
+
const search = input.toLowerCase();
|
|
41
|
+
return openApiEndpoints
|
|
42
|
+
.filter((endpoint) => {
|
|
43
|
+
const values = [endpoint.operationId, endpoint.path, endpoint.summary, endpoint.tags.join(' ')]
|
|
44
|
+
.join(' ')
|
|
45
|
+
.toLowerCase();
|
|
46
|
+
return values.includes(search);
|
|
47
|
+
})
|
|
48
|
+
.slice(0, 8);
|
|
49
|
+
}
|
|
50
|
+
export function endpointToSummary(endpoint) {
|
|
51
|
+
return {
|
|
52
|
+
operationId: endpoint.operationId,
|
|
53
|
+
method: endpoint.method,
|
|
54
|
+
path: endpoint.path,
|
|
55
|
+
tags: endpoint.tags,
|
|
56
|
+
summary: endpoint.summary,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output helpers. Default is compact JSON (for AI agents / piping).
|
|
3
|
+
* --pretty flag enables indented JSON for humans.
|
|
4
|
+
*/
|
|
5
|
+
/** Print data as JSON. Uses indentation when --pretty is set. */
|
|
6
|
+
export declare function output(data: unknown, pretty?: boolean): void;
|
|
7
|
+
export declare function outputWarning(message: string): void;
|
|
8
|
+
/** Print an error as structured JSON and exit with code 1. */
|
|
9
|
+
export declare function outputError(message: string, status?: number, extra?: Record<string, unknown>, pretty?: boolean): never;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output helpers. Default is compact JSON (for AI agents / piping).
|
|
3
|
+
* --pretty flag enables indented JSON for humans.
|
|
4
|
+
*/
|
|
5
|
+
/** Print data as JSON. Uses indentation when --pretty is set. */
|
|
6
|
+
export function output(data, pretty = false) {
|
|
7
|
+
if (pretty) {
|
|
8
|
+
console.log(JSON.stringify(data, null, 2));
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.log(JSON.stringify(data));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function outputWarning(message) {
|
|
15
|
+
process.stderr.write(`${message}\n`);
|
|
16
|
+
}
|
|
17
|
+
/** Print an error as structured JSON and exit with code 1. */
|
|
18
|
+
export function outputError(message, status, extra = {}, pretty = false) {
|
|
19
|
+
const err = { ok: false, error: true, message, ...extra };
|
|
20
|
+
if (status !== undefined)
|
|
21
|
+
err.status = status;
|
|
22
|
+
console.error(pretty ? JSON.stringify(err, null, 2) : JSON.stringify(err));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Shape
|
|
4
|
+
|
|
5
|
+
The CLI is a single TypeScript package.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
src/
|
|
9
|
+
commands/ yargs command groups
|
|
10
|
+
generated/ OpenAPI catalog generated from docs/openapi
|
|
11
|
+
utils/ config, output, HTTP request helpers
|
|
12
|
+
scripts/ build-time helpers
|
|
13
|
+
docs/openapi/ bundled Zernio OpenAPI spec
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Boundaries
|
|
17
|
+
|
|
18
|
+
- Curated commands use `@zernio/node` where the SDK offers stable workflow methods.
|
|
19
|
+
- `api:*` commands use raw HTTP so every OpenAPI endpoint is reachable.
|
|
20
|
+
- `media:upload` remains special because uploads require presign + direct PUT.
|
|
21
|
+
- No MCP server is included.
|
|
22
|
+
|
|
23
|
+
## Config
|
|
24
|
+
|
|
25
|
+
Resolution order:
|
|
26
|
+
1. CLI one-off flags where supported
|
|
27
|
+
2. `ZERNIO_API_KEY`, `ZERNIO_API_URL`
|
|
28
|
+
3. `~/.zernio/config.json`
|
|
29
|
+
4. deprecated `LATE_*` and `~/.late/config.json`
|
|
30
|
+
|
|
31
|
+
Diagnostics report secret sources only, never values. Cwd dotenv loading is opt-in through `ZERNIO_CLI_LOAD_ENV=1` for local development, not automatic in the published CLI.
|
|
32
|
+
|
|
33
|
+
## OpenAPI Generation
|
|
34
|
+
|
|
35
|
+
`npm run generate:openapi` reads `docs/openapi/zernio-api-openapi.yaml` and writes `src/generated/openapi-catalog.ts`.
|
|
36
|
+
|
|
37
|
+
Generated output is deterministic so build/test does not create timestamp churn.
|