sendcraft-cli 1.0.0 → 1.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/bin/sendcraft.js +7 -8
- package/lib/banner.js +53 -0
- package/lib/commands/campaigns.js +16 -15
- package/lib/commands/config.js +23 -15
- package/lib/commands/domains.js +49 -45
- package/lib/commands/emails.js +35 -31
- package/lib/commands/keys.js +32 -28
- package/lib/commands/mcp.js +77 -0
- package/lib/commands/send.js +8 -7
- package/lib/commands/subscribers.js +20 -21
- package/lib/commands/warmup.js +40 -19
- package/lib/interactive.js +408 -0
- package/lib/output.js +28 -6
- package/package.json +10 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const { Command } = require('commander');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const _grad2 = require('gradient-string'); const gradient = _grad2.default || _grad2;
|
|
4
|
+
const _boxen = require('boxen'); const boxen = _boxen.default || _boxen;
|
|
5
|
+
const { success, info } = require('../output');
|
|
6
|
+
|
|
7
|
+
const cmd = new Command('mcp').description('MCP (Model Context Protocol) server setup for AI agents');
|
|
8
|
+
|
|
9
|
+
cmd
|
|
10
|
+
.command('info')
|
|
11
|
+
.description('Show how to configure the SendCraft MCP server in Claude / Cursor / etc.')
|
|
12
|
+
.action(() => {
|
|
13
|
+
console.log('\n' + gradient(['#6366f1', '#8b5cf6', '#ec4899'])(' ✦ SendCraft MCP Server') + '\n');
|
|
14
|
+
|
|
15
|
+
const apiKey = process.env.SENDCRAFT_API_KEY || '<your-api-key>';
|
|
16
|
+
|
|
17
|
+
const claudeConfig = JSON.stringify({
|
|
18
|
+
mcpServers: {
|
|
19
|
+
sendcraft: {
|
|
20
|
+
command: 'npx',
|
|
21
|
+
args: ['sendcraft-mcp'],
|
|
22
|
+
env: {
|
|
23
|
+
SENDCRAFT_API_KEY: apiKey,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}, null, 2);
|
|
28
|
+
|
|
29
|
+
console.log(chalk.bold(' 1. Install the MCP server:'));
|
|
30
|
+
console.log(` ${chalk.cyan('npm install -g sendcraft-mcp')}\n`);
|
|
31
|
+
|
|
32
|
+
console.log(chalk.bold(' 2. Add to Claude Desktop config') + chalk.dim(' (~/Library/Application Support/Claude/claude_desktop_config.json):'));
|
|
33
|
+
console.log(boxen(claudeConfig, {
|
|
34
|
+
padding: 1,
|
|
35
|
+
margin: { left: 4 },
|
|
36
|
+
borderStyle: 'round',
|
|
37
|
+
borderColor: 'cyan',
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
console.log(chalk.bold(' 3. Available MCP tools:'));
|
|
41
|
+
const tools = [
|
|
42
|
+
['sendcraft_send_email', 'Send a transactional email'],
|
|
43
|
+
['sendcraft_batch_send', 'Send up to 100 emails at once'],
|
|
44
|
+
['sendcraft_list_emails', 'List sent emails'],
|
|
45
|
+
['sendcraft_get_stats', 'Get email statistics'],
|
|
46
|
+
['sendcraft_list_campaigns', 'List campaigns'],
|
|
47
|
+
['sendcraft_list_subscribers','List subscribers'],
|
|
48
|
+
['sendcraft_add_subscriber', 'Add a new subscriber'],
|
|
49
|
+
['sendcraft_list_domains', 'List verified sender domains'],
|
|
50
|
+
['sendcraft_list_api_keys', 'List API keys'],
|
|
51
|
+
];
|
|
52
|
+
tools.forEach(([name, desc]) => {
|
|
53
|
+
console.log(` ${chalk.green('•')} ${chalk.cyan(name)} ${chalk.dim('— ' + desc)}`);
|
|
54
|
+
});
|
|
55
|
+
console.log();
|
|
56
|
+
info(`Docs: ${chalk.underline('https://sendcraft.online/docs/mcp')}`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
cmd
|
|
60
|
+
.command('install')
|
|
61
|
+
.description('Install the sendcraft-mcp package globally')
|
|
62
|
+
.action(() => {
|
|
63
|
+
const { execSync } = require('child_process');
|
|
64
|
+
const ora = require('ora');
|
|
65
|
+
const sp = ora({ text: 'Installing sendcraft-mcp…', spinner: 'dots', color: 'cyan' }).start();
|
|
66
|
+
try {
|
|
67
|
+
execSync('npm install -g sendcraft-mcp', { stdio: 'pipe' });
|
|
68
|
+
sp.succeed(chalk.dim('Installed'));
|
|
69
|
+
success('sendcraft-mcp installed! Run ' + chalk.cyan('sendcraft mcp info') + ' for config instructions.');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
sp.fail(chalk.red('Install failed'));
|
|
72
|
+
console.error(e.stderr?.toString() || e.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
module.exports = cmd;
|
package/lib/commands/send.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const
|
|
2
|
+
const chalk = require('chalk');
|
|
3
3
|
const client = require('../client');
|
|
4
|
-
const { success, error,
|
|
4
|
+
const { success, error, spinner } = require('../output');
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('send')
|
|
7
7
|
.description('Send a single transactional email')
|
|
@@ -17,7 +17,8 @@ const cmd = new Command('send')
|
|
|
17
17
|
error('Provide at least --html or --text');
|
|
18
18
|
process.exit(1);
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
const sp = spinner(`Sending to ${chalk.cyan(opts.to)}…`).start();
|
|
21
22
|
try {
|
|
22
23
|
const result = await client.post('/emails/send', {
|
|
23
24
|
toEmail: opts.to,
|
|
@@ -27,11 +28,11 @@ const cmd = new Command('send')
|
|
|
27
28
|
fromEmail: opts.from,
|
|
28
29
|
replyTo: opts.replyTo,
|
|
29
30
|
});
|
|
30
|
-
|
|
31
|
-
if (opts.json) return json(result);
|
|
32
|
-
success(`Email sent!
|
|
31
|
+
sp.succeed(chalk.dim('Request complete'));
|
|
32
|
+
if (opts.json) return require('../output').json(result);
|
|
33
|
+
success(`Email sent! ${chalk.dim('ID: ' + (result.emailId || result._id || '—'))}`);
|
|
33
34
|
} catch (err) {
|
|
34
|
-
|
|
35
|
+
sp.fail(chalk.red('Send failed'));
|
|
35
36
|
error(err.message);
|
|
36
37
|
process.exit(1);
|
|
37
38
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const
|
|
2
|
+
const chalk = require('chalk');
|
|
3
3
|
const client = require('../client');
|
|
4
|
-
const { table, json, colorStatus, info, success, error } = require('../output');
|
|
4
|
+
const { table, json, colorStatus, info, success, error, spinner } = require('../output');
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('subscribers').description('Manage subscribers');
|
|
7
7
|
|
|
@@ -13,28 +13,28 @@ cmd
|
|
|
13
13
|
.option('--status <s>', 'Filter: active, pending, unsubscribed')
|
|
14
14
|
.option('--json', 'Output raw JSON')
|
|
15
15
|
.action(async (opts) => {
|
|
16
|
-
const
|
|
16
|
+
const sp = spinner('Fetching subscribers…').start();
|
|
17
17
|
try {
|
|
18
18
|
const params = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
19
19
|
if (opts.status) params.set('status', opts.status);
|
|
20
20
|
const data = await client.get(`/subscribers?${params}`);
|
|
21
|
-
|
|
21
|
+
sp.succeed(chalk.dim('Loaded'));
|
|
22
22
|
if (opts.json) return json(data);
|
|
23
23
|
const subs = data.subscribers || [];
|
|
24
24
|
if (!subs.length) return info('No subscribers found.');
|
|
25
25
|
table(
|
|
26
26
|
['Email', 'Name', 'Status', 'Tags', 'Joined'],
|
|
27
27
|
subs.map(s => [
|
|
28
|
-
s.email,
|
|
29
|
-
[s.firstName, s.lastName].filter(Boolean).join(' ') || '—',
|
|
28
|
+
chalk.cyan(s.email),
|
|
29
|
+
[s.firstName, s.lastName].filter(Boolean).join(' ') || chalk.dim('—'),
|
|
30
30
|
colorStatus(s.status),
|
|
31
|
-
(s.tags || []).join(', ')
|
|
32
|
-
s.createdAt ? new Date(s.createdAt).toLocaleDateString() : '—',
|
|
31
|
+
(s.tags || []).length ? s.tags.map(t => chalk.magenta(t)).join(', ') : chalk.dim('—'),
|
|
32
|
+
s.createdAt ? chalk.dim(new Date(s.createdAt).toLocaleDateString()) : '—',
|
|
33
33
|
])
|
|
34
34
|
);
|
|
35
|
-
info(`Page ${opts.page} · ${subs.length} of ${data.total
|
|
35
|
+
info(`Page ${opts.page} · ${subs.length} of ${chalk.bold(data.total ?? '?')}`);
|
|
36
36
|
} catch (err) {
|
|
37
|
-
|
|
37
|
+
sp.fail(chalk.red('Failed'));
|
|
38
38
|
error(err.message);
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
@@ -42,15 +42,14 @@ cmd
|
|
|
42
42
|
|
|
43
43
|
cmd
|
|
44
44
|
.command('add <email>')
|
|
45
|
-
.description('Add a subscriber')
|
|
46
|
-
.
|
|
45
|
+
.description('Add a subscriber to a list')
|
|
46
|
+
.requiredOption('--list <listId>', 'Email list ID')
|
|
47
47
|
.option('--first-name <name>', 'First name')
|
|
48
48
|
.option('--last-name <name>', 'Last name')
|
|
49
49
|
.option('--tags <tags>', 'Comma-separated tags')
|
|
50
50
|
.option('--json', 'Output raw JSON')
|
|
51
51
|
.action(async (email, opts) => {
|
|
52
|
-
|
|
53
|
-
const spinner = ora('Adding…').start();
|
|
52
|
+
const sp = spinner(`Adding ${chalk.cyan(email)}…`).start();
|
|
54
53
|
try {
|
|
55
54
|
const data = await client.post('/subscribers/add', {
|
|
56
55
|
email,
|
|
@@ -59,11 +58,11 @@ cmd
|
|
|
59
58
|
lastName: opts.lastName,
|
|
60
59
|
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : undefined,
|
|
61
60
|
});
|
|
62
|
-
|
|
61
|
+
sp.succeed(chalk.dim('Done'));
|
|
63
62
|
if (opts.json) return json(data);
|
|
64
|
-
success(
|
|
63
|
+
success(`${chalk.cyan(email)} added to list.`);
|
|
65
64
|
} catch (err) {
|
|
66
|
-
|
|
65
|
+
sp.fail(chalk.red('Failed'));
|
|
67
66
|
error(err.message);
|
|
68
67
|
process.exit(1);
|
|
69
68
|
}
|
|
@@ -74,14 +73,14 @@ cmd
|
|
|
74
73
|
.description('Remove a subscriber by ID')
|
|
75
74
|
.option('--json', 'Output raw JSON')
|
|
76
75
|
.action(async (id, opts) => {
|
|
77
|
-
const
|
|
76
|
+
const sp = spinner(`Removing ${chalk.dim(id.slice(-8))}…`).start();
|
|
78
77
|
try {
|
|
79
78
|
const data = await client.delete(`/subscribers/${id}`);
|
|
80
|
-
|
|
79
|
+
sp.succeed(chalk.dim('Done'));
|
|
81
80
|
if (opts.json) return json(data);
|
|
82
|
-
success(`Subscriber
|
|
81
|
+
success(`Subscriber removed.`);
|
|
83
82
|
} catch (err) {
|
|
84
|
-
|
|
83
|
+
sp.fail(chalk.red('Failed'));
|
|
85
84
|
error(err.message);
|
|
86
85
|
process.exit(1);
|
|
87
86
|
}
|
package/lib/commands/warmup.js
CHANGED
|
@@ -1,52 +1,73 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const ora = require('ora');
|
|
3
2
|
const chalk = require('chalk');
|
|
3
|
+
const _grad = require('gradient-string'); const gradient = _grad.default || _grad;
|
|
4
4
|
const client = require('../client');
|
|
5
|
-
const { table, json, error } = require('../output');
|
|
5
|
+
const { table, json, error, spinner } = require('../output');
|
|
6
6
|
|
|
7
7
|
const cmd = new Command('warmup').description('View SMTP IP warmup status');
|
|
8
8
|
|
|
9
9
|
cmd
|
|
10
10
|
.option('--json', 'Output raw JSON')
|
|
11
11
|
.action(async (opts) => {
|
|
12
|
-
const
|
|
12
|
+
const sp = spinner('Checking warmup status…').start();
|
|
13
13
|
try {
|
|
14
14
|
const data = await client.get('/smtp/warmup');
|
|
15
|
-
|
|
15
|
+
sp.succeed(chalk.dim('Done'));
|
|
16
|
+
|
|
16
17
|
if (opts.json) return json(data);
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
console.log();
|
|
19
20
|
|
|
20
21
|
if (data.isWarmedUp) {
|
|
21
|
-
|
|
22
|
+
const g = gradient(['#10b981', '#3b82f6']);
|
|
23
|
+
console.log(g.multiline(
|
|
24
|
+
' ██╗ ██╗ █████╗ ██████╗ ███╗ ███╗███████╗██████╗ \n' +
|
|
25
|
+
' ██║ ██║██╔══██╗██╔══██╗████╗ ████║██╔════╝██╔══██╗\n' +
|
|
26
|
+
' ██║ █╗ ██║███████║██████╔╝██╔████╔██║█████╗ ██║ ██║\n' +
|
|
27
|
+
' ██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║██╔══╝ ██║ ██║\n' +
|
|
28
|
+
' ╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║███████╗██████╔╝\n' +
|
|
29
|
+
' ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ '
|
|
30
|
+
));
|
|
31
|
+
console.log(' ' + chalk.green.bold('✓ IP fully warmed up — no daily limits!\n'));
|
|
22
32
|
} else {
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
const pct = data.percentComplete || 0;
|
|
34
|
+
const bar = buildAnimatedBar(data.todayCount, data.dailyLimit, 30);
|
|
35
|
+
console.log(` ${chalk.bold.magenta('Warmup Day ' + data.warmupDay)}`);
|
|
36
|
+
console.log(` ${bar} ${chalk.cyan(data.todayCount)}${chalk.dim('/' + data.dailyLimit)} sent today`);
|
|
37
|
+
console.log(` ${chalk.dim(pct + '% of warmup complete')}\n`);
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
table(
|
|
28
41
|
['Field', 'Value'],
|
|
29
42
|
[
|
|
30
|
-
['Warmup Day',
|
|
31
|
-
['Daily Limit',
|
|
32
|
-
['Sent Today',
|
|
33
|
-
['Remaining Today', data.isWarmedUp ? '∞' : data.remainingToday],
|
|
34
|
-
['Warmed Up', data.isWarmedUp ? chalk.green('Yes') : chalk.yellow('No')],
|
|
35
|
-
['
|
|
43
|
+
['Warmup Day', chalk.bold(data.warmupDay)],
|
|
44
|
+
['Daily Limit', data.isWarmedUp ? chalk.green('∞ Unlimited') : chalk.yellow(String(data.dailyLimit))],
|
|
45
|
+
['Sent Today', chalk.cyan(String(data.todayCount))],
|
|
46
|
+
['Remaining Today', data.isWarmedUp ? '∞' : chalk.green(String(data.remainingToday))],
|
|
47
|
+
['Fully Warmed Up', data.isWarmedUp ? chalk.green('Yes ✓') : chalk.yellow('No')],
|
|
48
|
+
['Progress', (data.percentComplete ?? 0) + '%'],
|
|
36
49
|
]
|
|
37
50
|
);
|
|
38
51
|
} catch (err) {
|
|
39
|
-
|
|
52
|
+
sp.fail(chalk.red('Failed to fetch warmup status'));
|
|
40
53
|
error(err.message);
|
|
41
54
|
process.exit(1);
|
|
42
55
|
}
|
|
43
56
|
});
|
|
44
57
|
|
|
45
|
-
function
|
|
58
|
+
function buildAnimatedBar(count, limit, width) {
|
|
46
59
|
if (!limit) return '';
|
|
47
|
-
const
|
|
48
|
-
const filled = Math.round(
|
|
49
|
-
|
|
60
|
+
const ratio = Math.min(count / limit, 1);
|
|
61
|
+
const filled = Math.round(ratio * width);
|
|
62
|
+
const empty = width - filled;
|
|
63
|
+
|
|
64
|
+
// Gradient bar: green→yellow→red based on fill level
|
|
65
|
+
let barColor;
|
|
66
|
+
if (ratio < 0.5) barColor = chalk.green;
|
|
67
|
+
else if (ratio < 0.85) barColor = chalk.yellow;
|
|
68
|
+
else barColor = chalk.red;
|
|
69
|
+
|
|
70
|
+
return chalk.dim('[') + barColor('█'.repeat(filled)) + chalk.dim('░'.repeat(empty)) + chalk.dim(']');
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
module.exports = cmd;
|