@vorlek/cli 0.1.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 ADDED
@@ -0,0 +1,33 @@
1
+ # vorlek-cli
2
+ Vorlek CLI — thin REST API wrapper
3
+
4
+ ## Install
5
+
6
+ ```sh
7
+ npm install -g @vorlek/cli
8
+ vorlek --version
9
+ ```
10
+
11
+ This package installs the `vorlek` command. The source repository can remain
12
+ private; first-time users should not need repository access to install or smoke
13
+ the CLI.
14
+
15
+ ## Commands
16
+
17
+ ```sh
18
+ vorlek auth signup
19
+ vorlek auth use --api-key vk_live_...
20
+ vorlek connect sendgrid --api-key SG....
21
+ vorlek catalog
22
+ vorlek contact upsert --provider sendgrid --email person@example.com --detail full
23
+ vorlek contact get --provider sendgrid --email person@example.com
24
+ vorlek operation get 01HV0000000000000000000000
25
+ vorlek template list --provider sendgrid
26
+ vorlek campaign list --provider mailchimp
27
+ vorlek email send --provider sendgrid --to person@example.com --subject "Hello" --text "Hi"
28
+ vorlek status --check
29
+ ```
30
+
31
+ Tool commands accept `--detail minimal|standard|full` where the API supports
32
+ response verbosity. Use `operation get` with the `meta.request_id` returned by
33
+ API responses to inspect a stored operation receipt.
package/dist/cli.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { cac } from 'cac';
3
+ import { registerAuthCommands } from './commands/auth.js';
4
+ import { registerCampaignCommands } from './commands/campaign.js';
5
+ import { registerCatalogCommands } from './commands/catalog.js';
6
+ import { registerConnectCommands } from './commands/connect.js';
7
+ import { registerConnectionCommands } from './commands/connection.js';
8
+ import { registerContactCommands } from './commands/contact.js';
9
+ import { registerEmailCommands } from './commands/email.js';
10
+ import { registerOperationCommands } from './commands/operation.js';
11
+ import { registerStatusCommand } from './commands/status.js';
12
+ import { registerTemplateCommands } from './commands/template.js';
13
+ import { defaultDeps } from './deps.js';
14
+ /**
15
+ * `vorlek` CLI entry. Sub-command surface per spec § 11:
16
+ * vorlek auth signup | use | whoami | logout | rotate
17
+ * vorlek connect <provider> | list | remove
18
+ * vorlek contact upsert
19
+ * vorlek status
20
+ *
21
+ * Global flags (--api-base, --json, -v/--verbose) are parsed here and
22
+ * threaded through `RunContext` so every command renders consistently.
23
+ */
24
+ const cli = cac('vorlek');
25
+ cli.help();
26
+ cli.version('0.1.0');
27
+ cli.option('--api-base <url>', 'Override API base URL (default: from config or https://api.vorlek.com)');
28
+ cli.option('--json', 'Output raw JSON (machine-readable)');
29
+ cli.option('-v, --verbose', 'Verbose logging (includes stack traces on errors)');
30
+ const deps = defaultDeps();
31
+ // Build the RunContext from cac's parsed options. cac populates these
32
+ // in `cli.options` AFTER parse(), but our command actions run after parse
33
+ // as well, so a closure that reads at call-time is fine.
34
+ const ctx = {
35
+ json: false,
36
+ verbose: false,
37
+ };
38
+ // Patch ctx in-place once we parse — cac's flow:
39
+ // 1. cli.command(...).action((args, opts) => {}) — action receives merged opts
40
+ // 2. cli.parse() returns the parsed result with the SAME options
41
+ // We mutate `ctx` from a top-level parse hook so command actions see the
42
+ // flags before they fire.
43
+ function bindCtxFromParsed(parsed) {
44
+ // biome-ignore lint/suspicious/noExplicitAny: cac's options bag is loosely typed
45
+ const opts = (parsed?.options ?? {});
46
+ ctx.json = Boolean(opts.json);
47
+ ctx.verbose = Boolean(opts.verbose ?? opts.v);
48
+ if (typeof opts['api-base'] === 'string')
49
+ ctx.apiBaseOverride = opts['api-base'];
50
+ else if (typeof opts.apiBase === 'string')
51
+ ctx.apiBaseOverride = opts.apiBase;
52
+ }
53
+ registerAuthCommands(cli, deps, ctx);
54
+ registerConnectionCommands(cli, deps, ctx);
55
+ registerConnectCommands(cli, deps, ctx);
56
+ registerContactCommands(cli, deps, ctx);
57
+ registerEmailCommands(cli, deps, ctx);
58
+ registerCampaignCommands(cli, deps, ctx);
59
+ registerTemplateCommands(cli, deps, ctx);
60
+ registerCatalogCommands(cli, deps, ctx);
61
+ registerOperationCommands(cli, deps, ctx);
62
+ registerStatusCommand(cli, deps, ctx);
63
+ cli.command('').action(() => {
64
+ cli.outputHelp();
65
+ });
66
+ const parsed = cli.parse(process.argv, { run: false });
67
+ bindCtxFromParsed(parsed);
68
+ try {
69
+ await cli.runMatchedCommand();
70
+ }
71
+ catch (err) {
72
+ // Last-resort handler for anything a command's try/catch missed.
73
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
74
+ process.exit(1);
75
+ }
76
+ //# sourceMappingURL=cli.js.map
package/dist/client.js ADDED
@@ -0,0 +1,148 @@
1
+ import ky from 'ky';
2
+ import { newIdempotencyKey } from './idempotency.js';
3
+ /**
4
+ * Phase 1.5.3 — typed API wrapper around `ky`. Every method either returns
5
+ * `data` from a SuccessEnvelope or throws `ApiError` carrying the server's
6
+ * error envelope (code/message/category/request_id). Callers decide how
7
+ * to render — the CLI's command handlers (1.5.4+) translate ApiError to
8
+ * exit codes per spec § 11.
9
+ */
10
+ export const DEFAULT_API_BASE = 'https://api.vorlek.com';
11
+ export class ApiError extends Error {
12
+ status;
13
+ envelope;
14
+ constructor(status, envelope) {
15
+ super(`${envelope.error.code}: ${envelope.error.message}`);
16
+ this.name = 'ApiError';
17
+ this.status = status;
18
+ this.envelope = envelope;
19
+ }
20
+ get code() {
21
+ return this.envelope.error.code;
22
+ }
23
+ get requestId() {
24
+ return this.envelope.meta.request_id;
25
+ }
26
+ }
27
+ export function createApiClient(opts) {
28
+ const headers = {};
29
+ if (opts.apiKey)
30
+ headers.Authorization = `Bearer ${opts.apiKey}`;
31
+ const http = ky.create({
32
+ prefixUrl: opts.apiBase,
33
+ timeout: opts.timeoutMs ?? 15_000,
34
+ retry: { limit: 2, methods: ['get'] },
35
+ headers,
36
+ throwHttpErrors: false, // we'll inspect status + envelope ourselves
37
+ hooks: {
38
+ beforeError: [
39
+ // Defensive — if throwHttpErrors flips on, ky pre-consumes body so
40
+ // err.response.text() can't be re-read. We're not relying on this
41
+ // path, but keep ky from doing surprising things on retry-failure.
42
+ ],
43
+ },
44
+ });
45
+ async function call(path, init) {
46
+ const res = await http(path, init);
47
+ const text = await res.text();
48
+ let body;
49
+ try {
50
+ body = JSON.parse(text);
51
+ }
52
+ catch {
53
+ throw new Error(`Server returned non-JSON (status ${res.status}): ${text.slice(0, 200)}`);
54
+ }
55
+ // The API uses two response shapes today (caught by 1.5 smoke):
56
+ // - Enveloped: { status: 'success' | 'error', data?, error?, meta }
57
+ // - Flat: { ...fields } directly (signup, account, connections list/delete)
58
+ // Detect by `.status` and `.error` presence rather than route, so a future
59
+ // server refactor that envelopes everything Just Works.
60
+ if (typeof body === 'object' && body !== null) {
61
+ const obj = body;
62
+ if (obj.status === 'error') {
63
+ throw new ApiError(res.status, body);
64
+ }
65
+ if (obj.status === 'success' && 'data' in obj) {
66
+ return body.data;
67
+ }
68
+ // Non-2xx without an error envelope = something unusual; surface it.
69
+ if (res.status < 200 || res.status >= 300) {
70
+ throw new Error(`Server returned status ${res.status} without an error envelope: ${text.slice(0, 200)}`);
71
+ }
72
+ // Flat success — body itself is the response.
73
+ return body;
74
+ }
75
+ throw new Error(`Server returned a non-object response: ${text.slice(0, 200)}`);
76
+ }
77
+ function toolPostInit(input) {
78
+ const { idempotencyKey, detail, ...json } = input;
79
+ return {
80
+ method: 'POST',
81
+ json,
82
+ headers: { 'Idempotency-Key': idempotencyKey ?? newIdempotencyKey() },
83
+ ...(detail ? { searchParams: { detail } } : {}),
84
+ };
85
+ }
86
+ return {
87
+ signup(input) {
88
+ return call('v1/accounts/signup', { method: 'POST', json: input });
89
+ },
90
+ rotateKey() {
91
+ return call('v1/account/api-keys/rotate', { method: 'POST' });
92
+ },
93
+ getAccount() {
94
+ return call('v1/account', { method: 'GET' });
95
+ },
96
+ createConnection(input) {
97
+ return call('v1/connections', { method: 'POST', json: input });
98
+ },
99
+ listConnections() {
100
+ return call('v1/connections', { method: 'GET' });
101
+ },
102
+ removeConnection(provider) {
103
+ return call(`v1/connections/${provider}`, {
104
+ method: 'DELETE',
105
+ });
106
+ },
107
+ getCatalog() {
108
+ return call('v1/catalog', { method: 'GET' });
109
+ },
110
+ getOperation(requestId) {
111
+ return call(`v1/operations/${encodeURIComponent(requestId)}`, {
112
+ method: 'GET',
113
+ });
114
+ },
115
+ upsertContact(input) {
116
+ return call('v1/tools/upsert_contact', toolPostInit(input));
117
+ },
118
+ getContact(input) {
119
+ return call('v1/tools/get_contact', toolPostInit(input));
120
+ },
121
+ getConnectionStatus(input) {
122
+ return call('v1/tools/get_connection_status', toolPostInit(input));
123
+ },
124
+ sendTransactional(input) {
125
+ return call('v1/tools/send_transactional', toolPostInit(input));
126
+ },
127
+ getCampaignStats(input) {
128
+ return call('v1/tools/get_campaign_stats', toolPostInit(input));
129
+ },
130
+ template: {
131
+ list(input) {
132
+ return call('v1/tools/list_templates', toolPostInit(input));
133
+ },
134
+ },
135
+ campaign: {
136
+ list(input) {
137
+ return call('v1/tools/list_campaigns', toolPostInit(input));
138
+ },
139
+ },
140
+ cleanupTestProfiles(input) {
141
+ return call(`v1/connections/${input.provider}/cleanup-test-profiles`, { method: 'POST', json: { pattern: input.pattern } });
142
+ },
143
+ getUsage() {
144
+ return call('v1/account/usage', { method: 'GET' });
145
+ },
146
+ };
147
+ }
148
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,157 @@
1
+ import pc from 'picocolors';
2
+ import { DEFAULT_API_BASE } from '../client.js';
3
+ import { ConfigMissingError, renderError, renderSuccess } from '../run-context.js';
4
+ export async function runSignup(deps, ctx) {
5
+ try {
6
+ const apiBase = ctx.apiBaseOverride ?? DEFAULT_API_BASE;
7
+ if (!ctx.json) {
8
+ deps.stdout.write(`${pc.bold('Vorlek signup')} — ${pc.dim(apiBase)}\n`);
9
+ }
10
+ const email = (await deps.prompts.text({
11
+ message: 'Email',
12
+ placeholder: 'you@example.com',
13
+ validate: (v) => (/^.+@.+\..+$/.test(v) ? undefined : 'Enter a valid email.'),
14
+ }));
15
+ if (deps.prompts.isCancel(email))
16
+ deps.exit(1);
17
+ const password = (await deps.prompts.password({
18
+ message: 'Password (min 8 chars)',
19
+ validate: (v) => (v && v.length >= 8 ? undefined : 'Password must be at least 8 chars.'),
20
+ }));
21
+ if (deps.prompts.isCancel(password))
22
+ deps.exit(1);
23
+ const confirm = (await deps.prompts.password({
24
+ message: 'Confirm password',
25
+ validate: (v) => (v === password ? undefined : 'Passwords do not match.'),
26
+ }));
27
+ if (deps.prompts.isCancel(confirm))
28
+ deps.exit(1);
29
+ const client = deps.createApiClient({ apiBase });
30
+ const data = await client.signup({ email: email, password: password });
31
+ await deps.saveConfig({
32
+ api_base: apiBase,
33
+ api_key: data.live_api_key,
34
+ account_id: data.account_id,
35
+ email: data.email,
36
+ created_at: data.created_at,
37
+ });
38
+ renderSuccess(deps, ctx, {
39
+ account_id: data.account_id,
40
+ email: data.email,
41
+ test_api_key: data.test_api_key,
42
+ }, () => [
43
+ `${pc.green('✓')} Signed up as ${pc.bold(data.email)}.`,
44
+ `Live key saved to ${pc.cyan('~/.vorlek/config.json')}.`,
45
+ '',
46
+ `${pc.yellow('!')} Save your ${pc.bold('test API key')} now — it is shown ONCE:`,
47
+ ` ${pc.bold(data.test_api_key)}`,
48
+ ].join('\n'));
49
+ }
50
+ catch (err) {
51
+ deps.exit(renderError(deps, ctx, err));
52
+ }
53
+ }
54
+ export async function runUse(deps, ctx, opts) {
55
+ try {
56
+ if (!opts.apiKey) {
57
+ throw new Error('Missing --api-key flag.');
58
+ }
59
+ if (!/^vk_(live|test)_/.test(opts.apiKey)) {
60
+ throw new Error('API key must start with vk_live_ or vk_test_.');
61
+ }
62
+ const apiBase = ctx.apiBaseOverride ?? DEFAULT_API_BASE;
63
+ const client = deps.createApiClient({ apiBase, apiKey: opts.apiKey });
64
+ const account = await client.getAccount();
65
+ await deps.saveConfig({
66
+ api_base: apiBase,
67
+ api_key: opts.apiKey,
68
+ account_id: account.account_id,
69
+ email: account.email,
70
+ created_at: account.created_at,
71
+ });
72
+ renderSuccess(deps, ctx, { account_id: account.account_id, email: account.email }, () => `${pc.green('✓')} Active API key set for ${pc.bold(account.email)} (${pc.dim(account.account_id)}).`);
73
+ }
74
+ catch (err) {
75
+ deps.exit(renderError(deps, ctx, err));
76
+ }
77
+ }
78
+ export async function runWhoami(deps, ctx) {
79
+ try {
80
+ const cfg = await deps.loadConfig();
81
+ if (!cfg)
82
+ throw new ConfigMissingError();
83
+ const apiBase = ctx.apiBaseOverride ?? cfg.api_base;
84
+ const client = deps.createApiClient({ apiBase, apiKey: cfg.api_key });
85
+ const [identity, usage] = await Promise.all([client.getAccount(), client.getUsage()]);
86
+ renderSuccess(deps, ctx, {
87
+ account_id: identity.account_id,
88
+ email: identity.email,
89
+ plan: identity.plan,
90
+ quota: usage.quota,
91
+ recent_operations: usage.recent_operations,
92
+ }, () => [
93
+ `${pc.bold('account')} ${identity.email} ${pc.dim(`(${identity.account_id})`)}`,
94
+ `${pc.bold('plan')} ${identity.plan}`,
95
+ `${pc.bold('usage')} ${usage.quota.used} / ${usage.quota.limit} this period (resets ${pc.dim(usage.quota.resets_at)})`,
96
+ `${pc.bold('errors')} ${usage.recent_operations.errors} of ${usage.recent_operations.total}`,
97
+ ].join('\n'));
98
+ }
99
+ catch (err) {
100
+ deps.exit(renderError(deps, ctx, err));
101
+ }
102
+ }
103
+ export async function runLogout(deps, ctx) {
104
+ try {
105
+ const removed = await deps.deleteConfig();
106
+ renderSuccess(deps, ctx, { removed }, () => removed
107
+ ? `${pc.green('✓')} Local config deleted. ${pc.yellow('Note:')} the API key is still active on the server — use ${pc.bold('vorlek auth rotate')} to invalidate it.`
108
+ : `${pc.dim('No config to remove.')}`);
109
+ }
110
+ catch (err) {
111
+ deps.exit(renderError(deps, ctx, err));
112
+ }
113
+ }
114
+ export async function runRotate(deps, ctx) {
115
+ try {
116
+ const cfg = await deps.loadConfig();
117
+ if (!cfg)
118
+ throw new ConfigMissingError();
119
+ const apiBase = ctx.apiBaseOverride ?? cfg.api_base;
120
+ const client = deps.createApiClient({ apiBase, apiKey: cfg.api_key });
121
+ const data = await client.rotateKey();
122
+ await deps.saveConfig({ ...cfg, api_key: data.live_api_key });
123
+ renderSuccess(deps, ctx, { rotated_at: data.rotated_at, test_api_key: data.test_api_key }, () => [
124
+ `${pc.green('✓')} Keys rotated at ${pc.dim(data.rotated_at)}.`,
125
+ `New live key saved to ${pc.cyan('~/.vorlek/config.json')}.`,
126
+ '',
127
+ `${pc.yellow('!')} New ${pc.bold('test API key')} — shown ONCE:`,
128
+ ` ${pc.bold(data.test_api_key)}`,
129
+ ].join('\n'));
130
+ }
131
+ catch (err) {
132
+ deps.exit(renderError(deps, ctx, err));
133
+ }
134
+ }
135
+ export function registerAuthCommands(cli, deps, ctx) {
136
+ cli
137
+ .command('auth <action>', 'Auth: signup | use | whoami | logout | rotate')
138
+ .option('--api-key <key>', 'API key (vk_live_... or vk_test_...) — required by `use`')
139
+ .action(async (action, opts) => {
140
+ switch (action) {
141
+ case 'signup':
142
+ return runSignup(deps, ctx);
143
+ case 'use':
144
+ return runUse(deps, ctx, opts);
145
+ case 'whoami':
146
+ return runWhoami(deps, ctx);
147
+ case 'logout':
148
+ return runLogout(deps, ctx);
149
+ case 'rotate':
150
+ return runRotate(deps, ctx);
151
+ default:
152
+ deps.stderr.write(`${pc.red('error')} Unknown auth action: ${action}. Try: signup | use | whoami | logout | rotate\n`);
153
+ deps.exit(1);
154
+ }
155
+ });
156
+ }
157
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,108 @@
1
+ import pc from 'picocolors';
2
+ import { ConfigMissingError, renderError, renderSuccess } from '../run-context.js';
3
+ import { parseDetail } from './detail.js';
4
+ import { resolveProvider } from './provider.js';
5
+ import { renderTable } from './table.js';
6
+ const ALL_PAGE_CAP = 100;
7
+ export async function runCampaignStats(deps, ctx, opts) {
8
+ try {
9
+ if (!opts.campaignId)
10
+ throw new Error('Missing --campaign-id flag.');
11
+ const cfg = await deps.loadConfig();
12
+ if (!cfg)
13
+ throw new ConfigMissingError();
14
+ const apiBase = ctx.apiBaseOverride ?? cfg.api_base;
15
+ const client = deps.createApiClient({ apiBase, apiKey: cfg.api_key });
16
+ if (!client.getCampaignStats)
17
+ throw new Error('Configured API client does not support get_campaign_stats.');
18
+ const provider = await resolveProvider(client, opts.provider);
19
+ const detail = parseDetail(opts.detail);
20
+ const result = await client.getCampaignStats({
21
+ provider,
22
+ campaign_id: opts.campaignId,
23
+ ...(detail ? { detail } : {}),
24
+ ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
25
+ });
26
+ renderSuccess(deps, ctx, result, () => `${pc.green('✓')} ${pc.bold(result.provider)} campaign ${pc.bold(result.campaign_id)}: ${result.sent} sent, ${result.opens} opens, ${result.clicks} clicks.`);
27
+ }
28
+ catch (err) {
29
+ deps.exit(renderError(deps, ctx, err));
30
+ }
31
+ }
32
+ export async function runCampaignList(deps, ctx, opts) {
33
+ try {
34
+ const cfg = await deps.loadConfig();
35
+ if (!cfg)
36
+ throw new ConfigMissingError();
37
+ const apiBase = ctx.apiBaseOverride ?? cfg.api_base;
38
+ const client = deps.createApiClient({ apiBase, apiKey: cfg.api_key });
39
+ if (!client.campaign?.list)
40
+ throw new Error('Configured API client does not support list_campaigns.');
41
+ const provider = await resolveProvider(client, opts.provider);
42
+ const status = opts.status ?? 'all';
43
+ const limit = parseLimit(opts.limit);
44
+ const detail = parseDetail(opts.detail);
45
+ const campaigns = [];
46
+ let cursor = opts.cursor ?? null;
47
+ let nextCursor = null;
48
+ for (let page = 0;; page++) {
49
+ if (page >= ALL_PAGE_CAP) {
50
+ throw new Error('--all reached the 100-page safety cap. Use --cursor to continue from the last seen page.');
51
+ }
52
+ const result = await client.campaign.list({
53
+ provider,
54
+ status,
55
+ ...(limit ? { limit } : {}),
56
+ ...(cursor ? { cursor } : {}),
57
+ ...(detail ? { detail } : {}),
58
+ ...(opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}),
59
+ });
60
+ campaigns.push(...result.campaigns);
61
+ nextCursor = result.next_cursor;
62
+ if (!opts.all || !nextCursor)
63
+ break;
64
+ cursor = nextCursor;
65
+ }
66
+ renderSuccess(deps, ctx, { provider, campaigns, next_cursor: nextCursor }, () => renderCampaigns(provider, campaigns, nextCursor));
67
+ }
68
+ catch (err) {
69
+ deps.exit(renderError(deps, ctx, err));
70
+ }
71
+ }
72
+ export function registerCampaignCommands(cli, deps, ctx) {
73
+ cli
74
+ .command('campaign <action>', 'Campaign operations: stats, list')
75
+ .option('--campaign-id <id>', 'Provider campaign id')
76
+ .option('--provider <name>', 'Provider override (defaults to auto-detect)')
77
+ .option('--status <status>', 'Campaign status: draft, scheduled, sent, all')
78
+ .option('--limit <n>', 'Page size, 1-100')
79
+ .option('--cursor <cursor>', 'Pagination cursor returned by the previous page')
80
+ .option('--all', 'Follow next_cursor until exhausted (max 100 pages)')
81
+ .option('--detail <level>', 'Response detail: minimal, standard, full')
82
+ .option('--idempotency-key <key>', 'Override the auto-generated idempotency key')
83
+ .action(async (action, opts) => {
84
+ if (action === 'stats')
85
+ return runCampaignStats(deps, ctx, opts);
86
+ if (action === 'list')
87
+ return runCampaignList(deps, ctx, opts);
88
+ deps.stderr.write(`${pc.red('error')} Unknown campaign action: ${action}. Try: stats, list\n`);
89
+ deps.exit(1);
90
+ });
91
+ }
92
+ function parseLimit(raw) {
93
+ if (raw === undefined)
94
+ return undefined;
95
+ const parsed = typeof raw === 'number' ? raw : Number.parseInt(raw, 10);
96
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
97
+ throw new Error('--limit must be an integer between 1 and 100.');
98
+ }
99
+ return parsed;
100
+ }
101
+ function renderCampaigns(provider, campaigns, nextCursor) {
102
+ if (campaigns.length === 0)
103
+ return pc.dim(`No campaigns found for ${provider}.`);
104
+ const table = renderTable(['id', 'name', 'status', 'updated_at'], campaigns.map((campaign) => [campaign.id, campaign.name, campaign.status, campaign.updated_at]));
105
+ const cursorHint = nextCursor ? `\n${pc.dim(`next_cursor: ${nextCursor}`)}` : '';
106
+ return `${pc.green('✓')} ${pc.bold(provider)} campaigns\n${table}${cursorHint}`;
107
+ }
108
+ //# sourceMappingURL=campaign.js.map
@@ -0,0 +1,53 @@
1
+ import pc from 'picocolors';
2
+ import { ConfigMissingError, renderError, renderSuccess } from '../run-context.js';
3
+ import { renderTable } from './table.js';
4
+ export async function runCatalogGet(deps, ctx) {
5
+ try {
6
+ const cfg = await deps.loadConfig();
7
+ if (!cfg)
8
+ throw new ConfigMissingError();
9
+ const apiBase = ctx.apiBaseOverride ?? cfg.api_base;
10
+ const client = deps.createApiClient({ apiBase, apiKey: cfg.api_key });
11
+ if (!client.getCatalog)
12
+ throw new Error('Configured API client does not support catalog.');
13
+ const result = await client.getCatalog();
14
+ renderSuccess(deps, ctx, result, () => renderCatalog(result));
15
+ }
16
+ catch (err) {
17
+ deps.exit(renderError(deps, ctx, err));
18
+ }
19
+ }
20
+ export function registerCatalogCommands(cli, deps, ctx) {
21
+ cli
22
+ .command('catalog [action]', 'Show account-aware provider and tool catalog')
23
+ .action(async (action) => {
24
+ if (action === undefined || action === 'get')
25
+ return runCatalogGet(deps, ctx);
26
+ deps.stderr.write(`${pc.red('error')} Unknown catalog action: ${action}. Try: get\n`);
27
+ deps.exit(1);
28
+ });
29
+ }
30
+ function renderCatalog(catalog) {
31
+ const providerTable = renderTable(['provider', 'status', 'tools'], catalog.providers.map((provider) => [
32
+ provider.provider,
33
+ provider.connection.status,
34
+ provider.supported_tools.join(', '),
35
+ ]));
36
+ const toolTable = renderTable(['tool', 'supported providers'], catalog.tools.map((tool) => [
37
+ tool.name,
38
+ tool.providers
39
+ .filter((provider) => provider.supported)
40
+ .map((provider) => provider.provider)
41
+ .join(', '),
42
+ ]));
43
+ return [
44
+ `${pc.green('✓')} Catalog ${pc.dim(`(${catalog.mode}, generated ${catalog.generated_at})`)}`,
45
+ '',
46
+ pc.bold('providers'),
47
+ providerTable,
48
+ '',
49
+ pc.bold('tools'),
50
+ toolTable,
51
+ ].join('\n');
52
+ }
53
+ //# sourceMappingURL=catalog.js.map