sendcraft-cli 1.1.0 → 1.2.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 +551 -0
- package/bin/sendcraft.js +19 -0
- package/lib/client.js +87 -1
- package/lib/commands/login.js +206 -0
- package/lib/config.js +21 -1
- package/lib/interactive.js +4 -2
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# sendcraft-cli
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://sendcraft.online/logo.png" alt="SendCraft" width="72" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Official CLI for <a href="https://sendcraft.online">SendCraft</a> — send emails, manage campaigns, verify domains, and more from your terminal.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/sendcraft-cli"><img src="https://img.shields.io/npm/v/sendcraft-cli?color=6366f1&label=npm" alt="npm version" /></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/sendcraft-cli"><img src="https://img.shields.io/npm/dm/sendcraft-cli?color=10b981&label=downloads" alt="downloads" /></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/node-%3E%3D14-brightgreen" alt="node" />
|
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT" />
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
███████╗███████╗███╗ ██╗██████╗ ██████╗██████╗ █████╗ ███████╗████████╗
|
|
22
|
+
██╔════╝██╔════╝████╗ ██║██╔══██╗██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
|
|
23
|
+
███████╗█████╗ ██╔██╗ ██║██║ ██║██║ ██████╔╝███████║█████╗ ██║
|
|
24
|
+
╚════██║██╔══╝ ██║╚██╗██║██║ ██║██║ ██╔══██╗██╔══██║██╔══╝ ██║
|
|
25
|
+
███████║███████╗██║ ╚████║██████╔╝╚██████╗██║ ██║██║ ██║██║ ██║
|
|
26
|
+
╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- 📧 **Send emails** directly from the terminal
|
|
32
|
+
- 📊 **View stats** — opens, clicks, bounces, delivery rates
|
|
33
|
+
- 📣 **Manage campaigns** — list and send with optional scheduling
|
|
34
|
+
- 👥 **Manage subscribers** — list, add, remove with tag support
|
|
35
|
+
- 🌐 **Domain verification** — add domains and see DNS records to configure
|
|
36
|
+
- 🔑 **API key management** — create, list, revoke with permission scopes
|
|
37
|
+
- 🔥 **IP warmup status** — animated progress bar, daily limits at a glance
|
|
38
|
+
- 🤖 **MCP setup** — one-command config for Claude Desktop AI agent integration
|
|
39
|
+
- 🎨 **Beautiful output** — gradient colors, spinners, tables, JSON mode
|
|
40
|
+
- 🖥️ **Interactive TUI** — arrow-key menu when launched with no arguments
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g sendcraft-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or run without installing:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx sendcraft-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 1. Log in (opens a menu — browser redirect or email+password)
|
|
62
|
+
sendcraft login
|
|
63
|
+
|
|
64
|
+
# 2. Send your first email
|
|
65
|
+
sendcraft send --to user@example.com --subject "Hello!" --html "<h1>It works!</h1>"
|
|
66
|
+
|
|
67
|
+
# 3. Check your stats
|
|
68
|
+
sendcraft emails stats
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sendcraft login # Interactive login (browser or email+password)
|
|
77
|
+
sendcraft login --browser # Open dashboard to copy an API key
|
|
78
|
+
sendcraft login --api # Sign in with email & password
|
|
79
|
+
sendcraft logout # Remove saved API key from this machine
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Browser flow
|
|
83
|
+
`sendcraft login --browser` opens `{your-base-url}/dashboard/settings` in your default browser.
|
|
84
|
+
Create or copy an existing API key from the **API Keys** tab, then paste it back in the terminal.
|
|
85
|
+
The key is verified against the API before being saved.
|
|
86
|
+
|
|
87
|
+
### Email & Password flow
|
|
88
|
+
`sendcraft login --api` prompts for your email and password, signs you in via the API, then
|
|
89
|
+
automatically creates a named **"CLI"** key with full access and saves it locally.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
sendcraft config init # Interactive setup wizard
|
|
97
|
+
sendcraft config set-key <apiKey> # Set your API key manually
|
|
98
|
+
sendcraft config set-url <url> # Override API base URL (self-hosted)
|
|
99
|
+
sendcraft config show # Print current config
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Config is stored in `~/.sendcraft/config.json`.
|
|
103
|
+
|
|
104
|
+
You can also use an environment variable — it takes precedence over the config file:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export SENDCRAFT_API_KEY=sc_live_xxxxxxxxxxxxxxxx
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Commands
|
|
113
|
+
|
|
114
|
+
### `sendcraft send` — Send an email
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
sendcraft send --to user@example.com --subject "Hello" --html "<h1>Hi</h1>"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Flag | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `-t, --to <email>` | **Required.** Recipient email address |
|
|
123
|
+
| `-s, --subject <text>` | **Required.** Email subject line |
|
|
124
|
+
| `-H, --html <html>` | HTML body (required if no `--text`) |
|
|
125
|
+
| `-T, --text <text>` | Plain text body (required if no `--html`) |
|
|
126
|
+
| `-f, --from <email>` | From address (uses account default if omitted) |
|
|
127
|
+
| `-r, --reply-to <email>` | Reply-To address |
|
|
128
|
+
| `--json` | Output raw JSON response |
|
|
129
|
+
|
|
130
|
+
**Examples:**
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Send HTML email
|
|
134
|
+
sendcraft send -t alice@example.com -s "Welcome!" -H "<h1>Hey Alice 👋</h1>"
|
|
135
|
+
|
|
136
|
+
# Send plain text
|
|
137
|
+
sendcraft send -t bob@example.com -s "Quick note" -T "Hey Bob, just checking in."
|
|
138
|
+
|
|
139
|
+
# Send with custom from + reply-to
|
|
140
|
+
sendcraft send -t user@example.com -s "Hi" -H "<p>Hello</p>" \
|
|
141
|
+
-f hello@myapp.com -r support@myapp.com
|
|
142
|
+
|
|
143
|
+
# Output JSON (good for scripting)
|
|
144
|
+
sendcraft send -t user@example.com -s "Test" -T "Body" --json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
### `sendcraft emails` — Manage emails
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
sendcraft emails list # List recent sent emails
|
|
153
|
+
sendcraft emails stats # Delivery stats summary
|
|
154
|
+
sendcraft emails get <emailId> # Details for a single email
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `emails list`
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
sendcraft emails list
|
|
161
|
+
sendcraft emails list --limit 50
|
|
162
|
+
sendcraft emails list --status delivered
|
|
163
|
+
sendcraft emails list --page 2 --json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Flag | Description |
|
|
167
|
+
|------|-------------|
|
|
168
|
+
| `-p, --page <n>` | Page number (default: 1) |
|
|
169
|
+
| `-l, --limit <n>` | Items per page (default: 20) |
|
|
170
|
+
| `--status <s>` | Filter: `sent`, `delivered`, `failed`, `scheduled` |
|
|
171
|
+
| `--json` | Output raw JSON |
|
|
172
|
+
|
|
173
|
+
#### `emails stats`
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
sendcraft emails stats
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Shows total sent, delivered, opened, clicked, bounced, failed, open rate, and click rate.
|
|
180
|
+
|
|
181
|
+
#### `emails get`
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
sendcraft emails get 64f2a1b3c9e4d50012345678
|
|
185
|
+
sendcraft emails get 64f2a1b3c9e4d50012345678 --json
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### `sendcraft campaigns` — Manage campaigns
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sendcraft campaigns list # List all campaigns
|
|
194
|
+
sendcraft campaigns send <campaignId> # Send a campaign now
|
|
195
|
+
sendcraft campaigns send <campaignId> --schedule 2025-12-25T09:00:00Z
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Flag | Description |
|
|
199
|
+
|------|-------------|
|
|
200
|
+
| `--schedule <isoDate>` | Schedule for a specific time (ISO 8601) |
|
|
201
|
+
| `--json` | Output raw JSON |
|
|
202
|
+
|
|
203
|
+
**Examples:**
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# List campaigns
|
|
207
|
+
sendcraft campaigns list
|
|
208
|
+
|
|
209
|
+
# Send immediately
|
|
210
|
+
sendcraft campaigns send 64f2a1b3c9e4d50012345678
|
|
211
|
+
|
|
212
|
+
# Schedule for Christmas morning
|
|
213
|
+
sendcraft campaigns send 64f2a1b3c9e4d50012345678 \
|
|
214
|
+
--schedule 2025-12-25T09:00:00Z
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
> Create campaigns in the [dashboard](https://sendcraft.online/dashboard/campaigns) — the CLI handles sending and scheduling.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### `sendcraft subscribers` — Manage subscribers
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
sendcraft subscribers list # List all subscribers
|
|
225
|
+
sendcraft subscribers add <email> --list <listId> # Add a subscriber
|
|
226
|
+
sendcraft subscribers remove <subscriberId> # Remove a subscriber
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `subscribers list`
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
sendcraft subscribers list
|
|
233
|
+
sendcraft subscribers list --status active --limit 50
|
|
234
|
+
sendcraft subscribers list --page 3 --json
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| Flag | Description |
|
|
238
|
+
|------|-------------|
|
|
239
|
+
| `-p, --page <n>` | Page number (default: 1) |
|
|
240
|
+
| `-l, --limit <n>` | Items per page (default: 20) |
|
|
241
|
+
| `--status <s>` | Filter: `active`, `pending`, `unsubscribed` |
|
|
242
|
+
| `--json` | Output raw JSON |
|
|
243
|
+
|
|
244
|
+
#### `subscribers add`
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
sendcraft subscribers add alice@example.com --list <listId>
|
|
248
|
+
sendcraft subscribers add bob@example.com --list <listId> \
|
|
249
|
+
--first-name Bob --last-name Smith --tags "vip,beta"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| Flag | Description |
|
|
253
|
+
|------|-------------|
|
|
254
|
+
| `--list <listId>` | **Required.** Target email list ID |
|
|
255
|
+
| `--first-name <name>` | First name |
|
|
256
|
+
| `--last-name <name>` | Last name |
|
|
257
|
+
| `--tags <tags>` | Comma-separated tags |
|
|
258
|
+
| `--json` | Output raw JSON |
|
|
259
|
+
|
|
260
|
+
#### `subscribers remove`
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
sendcraft subscribers remove 64f2a1b3c9e4d50012345678
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### `sendcraft domains` — Manage sender domains
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
sendcraft domains list # List all domains
|
|
272
|
+
sendcraft domains add <domain> # Add a new domain + show DNS records
|
|
273
|
+
sendcraft domains verify <domainId> # Re-check DNS verification
|
|
274
|
+
sendcraft domains records <domainId> # Show DNS records to configure
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Full workflow:**
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
# 1. Add your domain
|
|
281
|
+
sendcraft domains add mail.myapp.com
|
|
282
|
+
|
|
283
|
+
# Output shows DNS records to add:
|
|
284
|
+
# SPF — TXT _spf.mail.myapp.com
|
|
285
|
+
# DKIM — TXT sendcraft._domainkey.mail.myapp.com
|
|
286
|
+
# DMARC — TXT _dmarc.mail.myapp.com
|
|
287
|
+
# BIMI — TXT default._bimi.mail.myapp.com (optional)
|
|
288
|
+
|
|
289
|
+
# 2. Add those records in your DNS provider, then:
|
|
290
|
+
sendcraft domains verify <domainId>
|
|
291
|
+
|
|
292
|
+
# Check again anytime:
|
|
293
|
+
sendcraft domains records <domainId>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
> DNS propagation can take up to 48 hours.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### `sendcraft keys` — Manage API keys
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
sendcraft keys list # List all API keys
|
|
304
|
+
sendcraft keys create <name> # Create a new key
|
|
305
|
+
sendcraft keys revoke <keyId> # Permanently revoke a key
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### `keys create`
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Full access key (default)
|
|
312
|
+
sendcraft keys create "Production"
|
|
313
|
+
|
|
314
|
+
# Send-only key (cannot manage campaigns, templates, subscribers)
|
|
315
|
+
sendcraft keys create "Frontend App" --permissions sending_access
|
|
316
|
+
|
|
317
|
+
# Restrict to specific sender domains
|
|
318
|
+
sendcraft keys create "Marketing Tool" --domains "newsletter.myapp.com,promo.myapp.com"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
| Flag | Description |
|
|
322
|
+
|------|-------------|
|
|
323
|
+
| `--permissions <type>` | `full_access` (default) or `sending_access` |
|
|
324
|
+
| `--domains <list>` | Comma-separated allowed sender domains |
|
|
325
|
+
| `--json` | Output raw JSON |
|
|
326
|
+
|
|
327
|
+
> ⚠️ The key is shown **once only**. Copy it immediately.
|
|
328
|
+
|
|
329
|
+
#### `keys revoke`
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
sendcraft keys revoke 64f2a1b3c9e4d50012345678
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### `sendcraft warmup` — IP warmup status
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
sendcraft warmup
|
|
341
|
+
sendcraft warmup --json
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Shows an animated progress bar, current warmup day, today's send count vs daily limit, and percentage complete.
|
|
345
|
+
|
|
346
|
+
**Warmup schedule:**
|
|
347
|
+
|
|
348
|
+
| Day | Daily limit |
|
|
349
|
+
|-----|-------------|
|
|
350
|
+
| 1 | 50 |
|
|
351
|
+
| 2 | 100 |
|
|
352
|
+
| 3 | 250 |
|
|
353
|
+
| 4 | 500 |
|
|
354
|
+
| 5 | 1,000 |
|
|
355
|
+
| 7 | 3,500 |
|
|
356
|
+
| 14 | 7,500 |
|
|
357
|
+
| 21 | 15,000 |
|
|
358
|
+
| 30 | 35,000 |
|
|
359
|
+
| 45 | 75,000 |
|
|
360
|
+
| 60+ | **Unlimited** |
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### `sendcraft mcp` — MCP server for AI agents
|
|
365
|
+
|
|
366
|
+
Connect SendCraft to **Claude Desktop** (or any MCP-compatible AI agent) so it can send emails on your behalf — no API calls needed.
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
sendcraft mcp info # Print Claude Desktop config snippet
|
|
370
|
+
sendcraft mcp install # Install sendcraft-mcp globally
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Setup in 2 steps
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
# 1. Install the MCP server
|
|
377
|
+
sendcraft mcp install
|
|
378
|
+
|
|
379
|
+
# 2. Print the config to add to Claude Desktop
|
|
380
|
+
sendcraft mcp info
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Paste the printed JSON into:
|
|
384
|
+
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
385
|
+
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
386
|
+
|
|
387
|
+
After restarting Claude Desktop, you can say:
|
|
388
|
+
|
|
389
|
+
> *"Send a welcome email to alice@example.com"*
|
|
390
|
+
|
|
391
|
+
...and Claude will call `sendcraft_send_email` natively.
|
|
392
|
+
|
|
393
|
+
**Available MCP tools:** `sendcraft_send_email`, `sendcraft_batch_send`, `sendcraft_list_emails`, `sendcraft_get_stats`, `sendcraft_list_campaigns`, `sendcraft_send_campaign`, `sendcraft_list_subscribers`, `sendcraft_add_subscriber`, `sendcraft_list_templates`, `sendcraft_list_domains`, `sendcraft_verify_domain`, `sendcraft_get_warmup_status`, and more.
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Interactive Mode
|
|
398
|
+
|
|
399
|
+
Launch `sendcraft` with no arguments to open a full-screen arrow-key TUI:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
sendcraft
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
✦ SendCraft CLI v1.1.0
|
|
407
|
+
|
|
408
|
+
? What would you like to do?
|
|
409
|
+
❯ 📧 Send Email
|
|
410
|
+
📋 Emails
|
|
411
|
+
📣 Campaigns
|
|
412
|
+
👥 Subscribers
|
|
413
|
+
🌐 Domains
|
|
414
|
+
🔑 API Keys
|
|
415
|
+
🔥 IP Warmup
|
|
416
|
+
🤖 MCP Setup
|
|
417
|
+
🔐 Login
|
|
418
|
+
⚙️ Config
|
|
419
|
+
✗ Exit
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Navigate with **↑ ↓** arrow keys, select with **Enter**.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## JSON Output
|
|
427
|
+
|
|
428
|
+
Every command supports `--json` for machine-readable output — useful in shell scripts and CI/CD pipelines:
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
# Get email stats as JSON
|
|
432
|
+
sendcraft emails stats --json | jq '.stats.openRate'
|
|
433
|
+
|
|
434
|
+
# List campaigns as JSON
|
|
435
|
+
sendcraft campaigns list --json | jq '.campaigns[].name'
|
|
436
|
+
|
|
437
|
+
# Pipe email ID after sending
|
|
438
|
+
ID=$(sendcraft send -t x@y.com -s "Hi" -T "Hello" --json | jq -r '.emailId')
|
|
439
|
+
echo "Sent: $ID"
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## CI/CD Usage
|
|
445
|
+
|
|
446
|
+
Use `SENDCRAFT_API_KEY` as an environment variable — no config file needed:
|
|
447
|
+
|
|
448
|
+
```yaml
|
|
449
|
+
# GitHub Actions example
|
|
450
|
+
- name: Send deployment notification
|
|
451
|
+
env:
|
|
452
|
+
SENDCRAFT_API_KEY: ${{ secrets.SENDCRAFT_API_KEY }}
|
|
453
|
+
run: |
|
|
454
|
+
npx sendcraft-cli send \
|
|
455
|
+
--to team@myapp.com \
|
|
456
|
+
--subject "🚀 Deployed to production" \
|
|
457
|
+
--html "<p>Deploy complete. Commit: ${{ github.sha }}</p>"
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Shell script
|
|
462
|
+
export SENDCRAFT_API_KEY=sc_live_xxx
|
|
463
|
+
|
|
464
|
+
npx sendcraft-cli send \
|
|
465
|
+
--to "$RECIPIENT" \
|
|
466
|
+
--subject "Build $BUILD_NUMBER passed" \
|
|
467
|
+
--text "All tests passed. See $BUILD_URL"
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Environment Variables
|
|
473
|
+
|
|
474
|
+
| Variable | Description |
|
|
475
|
+
|----------|-------------|
|
|
476
|
+
| `SENDCRAFT_API_KEY` | API key — overrides config file |
|
|
477
|
+
| `SENDCRAFT_API_URL` | API base URL — for self-hosted deployments |
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Self-Hosted / Custom API URL
|
|
482
|
+
|
|
483
|
+
If you run your own SendCraft instance:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
sendcraft config set-url https://api.myinstance.com/api
|
|
487
|
+
# or
|
|
488
|
+
export SENDCRAFT_API_URL=https://api.myinstance.com/api
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## All Commands at a Glance
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
sendcraft Interactive TUI (no args)
|
|
497
|
+
|
|
498
|
+
sendcraft login Log in (browser or email+password)
|
|
499
|
+
sendcraft login --browser Open dashboard to copy API key
|
|
500
|
+
sendcraft login --api Sign in with email & password
|
|
501
|
+
sendcraft logout Remove saved API key
|
|
502
|
+
|
|
503
|
+
sendcraft config init Interactive config wizard
|
|
504
|
+
sendcraft config set-key <key> Set API key manually
|
|
505
|
+
sendcraft config set-url <url> Set API base URL
|
|
506
|
+
sendcraft config show Show current config
|
|
507
|
+
|
|
508
|
+
sendcraft send Send a transactional email
|
|
509
|
+
-t <email> -s <subject> -H <html>
|
|
510
|
+
|
|
511
|
+
sendcraft emails list List sent emails
|
|
512
|
+
sendcraft emails stats Delivery stats
|
|
513
|
+
sendcraft emails get <id> Single email details
|
|
514
|
+
|
|
515
|
+
sendcraft campaigns list List campaigns
|
|
516
|
+
sendcraft campaigns send <id> Send / schedule a campaign
|
|
517
|
+
|
|
518
|
+
sendcraft subscribers list List subscribers
|
|
519
|
+
sendcraft subscribers add <email> Add a subscriber
|
|
520
|
+
sendcraft subscribers remove <id> Remove a subscriber
|
|
521
|
+
|
|
522
|
+
sendcraft domains list List domains
|
|
523
|
+
sendcraft domains add <domain> Add domain + show DNS records
|
|
524
|
+
sendcraft domains verify <id> Trigger DNS check
|
|
525
|
+
sendcraft domains records <id> Show DNS records
|
|
526
|
+
|
|
527
|
+
sendcraft keys list List API keys
|
|
528
|
+
sendcraft keys create <name> Create API key
|
|
529
|
+
sendcraft keys revoke <id> Revoke API key
|
|
530
|
+
|
|
531
|
+
sendcraft warmup IP warmup status
|
|
532
|
+
|
|
533
|
+
sendcraft mcp info Claude Desktop config snippet
|
|
534
|
+
sendcraft mcp install Install sendcraft-mcp globally
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## Related Packages
|
|
540
|
+
|
|
541
|
+
| Package | Description |
|
|
542
|
+
|---------|-------------|
|
|
543
|
+
| [`sendcraft-sdk`](https://www.npmjs.com/package/sendcraft-sdk) | Node.js / TypeScript SDK |
|
|
544
|
+
| [`sendcraft-sdk` (PyPI)](https://pypi.org/project/sendcraft-sdk/) | Python SDK |
|
|
545
|
+
| [`sendcraft-mcp`](https://www.npmjs.com/package/sendcraft-mcp) | MCP server for AI agents |
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## License
|
|
550
|
+
|
|
551
|
+
MIT © [SendCraft](https://sendcraft.online)
|
package/bin/sendcraft.js
CHANGED
|
@@ -11,6 +11,25 @@ program
|
|
|
11
11
|
.description('Official SendCraft CLI')
|
|
12
12
|
.version(pkg.version, '-v, --version');
|
|
13
13
|
|
|
14
|
+
program.addCommand(require('../lib/commands/login'));
|
|
15
|
+
|
|
16
|
+
// sendcraft logout — convenience alias that clears the stored API key
|
|
17
|
+
const { Command } = require('commander');
|
|
18
|
+
const logoutCmd = new Command('logout')
|
|
19
|
+
.description('Remove the stored API key from this machine')
|
|
20
|
+
.action(() => {
|
|
21
|
+
const { load, save, CONFIG_FILE } = require('../lib/config');
|
|
22
|
+
const cfg = load();
|
|
23
|
+
if (!cfg.api_key) {
|
|
24
|
+
console.log(chalk.yellow(' ⚠ No API key stored — already logged out.'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
delete cfg.api_key;
|
|
28
|
+
save(cfg);
|
|
29
|
+
console.log(chalk.green(' ✓ ') + chalk.bold('Logged out.') + chalk.dim(' Key removed from ' + CONFIG_FILE));
|
|
30
|
+
});
|
|
31
|
+
program.addCommand(logoutCmd);
|
|
32
|
+
|
|
14
33
|
program.addCommand(require('../lib/commands/config'));
|
|
15
34
|
program.addCommand(require('../lib/commands/send'));
|
|
16
35
|
program.addCommand(require('../lib/commands/emails'));
|
package/lib/client.js
CHANGED
|
@@ -7,11 +7,94 @@ const http = require('http');
|
|
|
7
7
|
const { URL } = require('url');
|
|
8
8
|
const { getApiKey, getBaseUrl } = require('./config');
|
|
9
9
|
|
|
10
|
+
/** Make a request with an explicit bearer token instead of stored API key */
|
|
11
|
+
function requestWithToken(method, path, body, token) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const fullUrl = new URL(getBaseUrl() + path);
|
|
14
|
+
const isHttps = fullUrl.protocol === 'https:';
|
|
15
|
+
const mod = isHttps ? require('https') : require('http');
|
|
16
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
17
|
+
const options = {
|
|
18
|
+
hostname: fullUrl.hostname,
|
|
19
|
+
port: fullUrl.port || (isHttps ? 443 : 80),
|
|
20
|
+
path: fullUrl.pathname + fullUrl.search,
|
|
21
|
+
method,
|
|
22
|
+
headers: {
|
|
23
|
+
'Authorization': `Bearer ${token}`,
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const req = mod.request(options, (res) => {
|
|
29
|
+
let data = '';
|
|
30
|
+
res.on('data', (chunk) => (data += chunk));
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
try {
|
|
33
|
+
const json = JSON.parse(data);
|
|
34
|
+
if (res.statusCode >= 400) {
|
|
35
|
+
const err = new Error(json.error || json.message || `HTTP ${res.statusCode}`);
|
|
36
|
+
err.status = res.statusCode;
|
|
37
|
+
err.body = json;
|
|
38
|
+
return reject(err);
|
|
39
|
+
}
|
|
40
|
+
resolve(json);
|
|
41
|
+
} catch {
|
|
42
|
+
reject(new Error(`Invalid JSON response (status ${res.statusCode})`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
req.on('error', reject);
|
|
47
|
+
if (payload) req.write(payload);
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Make a request with no auth header (for public endpoints like /auth/login) */
|
|
53
|
+
function requestNoAuth(method, path, body = null) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const fullUrl = new URL(getBaseUrl() + path);
|
|
56
|
+
const isHttps = fullUrl.protocol === 'https:';
|
|
57
|
+
const mod = isHttps ? require('https') : require('http');
|
|
58
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
59
|
+
const options = {
|
|
60
|
+
hostname: fullUrl.hostname,
|
|
61
|
+
port: fullUrl.port || (isHttps ? 443 : 80),
|
|
62
|
+
path: fullUrl.pathname + fullUrl.search,
|
|
63
|
+
method,
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json',
|
|
66
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const req = mod.request(options, (res) => {
|
|
70
|
+
let data = '';
|
|
71
|
+
res.on('data', (chunk) => (data += chunk));
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
try {
|
|
74
|
+
const json = JSON.parse(data);
|
|
75
|
+
if (res.statusCode >= 400) {
|
|
76
|
+
const err = new Error(json.error || json.message || `HTTP ${res.statusCode}`);
|
|
77
|
+
err.status = res.statusCode;
|
|
78
|
+
err.body = json;
|
|
79
|
+
return reject(err);
|
|
80
|
+
}
|
|
81
|
+
resolve(json);
|
|
82
|
+
} catch {
|
|
83
|
+
reject(new Error(`Invalid JSON response (status ${res.statusCode})`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
req.on('error', reject);
|
|
88
|
+
if (payload) req.write(payload);
|
|
89
|
+
req.end();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
10
93
|
function request(method, path, body = null) {
|
|
11
94
|
return new Promise((resolve, reject) => {
|
|
12
95
|
const apiKey = getApiKey();
|
|
13
96
|
if (!apiKey) {
|
|
14
|
-
return reject(new Error('No API key configured. Run: sendcraft
|
|
97
|
+
return reject(new Error('No API key configured. Run: sendcraft login'));
|
|
15
98
|
}
|
|
16
99
|
|
|
17
100
|
const fullUrl = new URL(getBaseUrl() + path);
|
|
@@ -60,4 +143,7 @@ module.exports = {
|
|
|
60
143
|
get: (path) => request('GET', path),
|
|
61
144
|
post: (path, body) => request('POST', path, body),
|
|
62
145
|
delete: (path) => request('DELETE', path),
|
|
146
|
+
postNoAuth: (path, body) => requestNoAuth('POST', path, body),
|
|
147
|
+
postWithToken: (path, body, token) => requestWithToken('POST', path, body, token),
|
|
148
|
+
getWithToken: (path, token) => requestWithToken('GET', path, null, token),
|
|
63
149
|
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sendcraft login
|
|
3
|
+
*
|
|
4
|
+
* Two flows:
|
|
5
|
+
* 1. Browser — opens the dashboard settings page so the user can copy their API key
|
|
6
|
+
* 2. Email + Password — calls /auth/login, then auto-creates a "CLI" API key
|
|
7
|
+
*
|
|
8
|
+
* URLs are derived from the configured base URL — no domains hardcoded.
|
|
9
|
+
*/
|
|
10
|
+
const { Command } = require('commander');
|
|
11
|
+
const prompts = require('prompts');
|
|
12
|
+
const chalk = require('chalk');
|
|
13
|
+
const { load, save, getWebUrl, CONFIG_FILE } = require('../config');
|
|
14
|
+
const client = require('../client');
|
|
15
|
+
const { success, error, info, warn, spinner } = require('../output');
|
|
16
|
+
const { sendcraftGradient } = require('../banner');
|
|
17
|
+
|
|
18
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Open a URL in the system's default browser (cross-platform). */
|
|
21
|
+
function openBrowser(url) {
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
try {
|
|
24
|
+
const cmd =
|
|
25
|
+
process.platform === 'darwin' ? `open "${url}"` :
|
|
26
|
+
process.platform === 'win32' ? `start "" "${url}"` :
|
|
27
|
+
`xdg-open "${url}"`;
|
|
28
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── browser flow ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
async function browserFlow() {
|
|
38
|
+
const webUrl = getWebUrl();
|
|
39
|
+
const settingsUrl = `${webUrl}/dashboard/settings`;
|
|
40
|
+
|
|
41
|
+
console.log('\n' + sendcraftGradient(' ✦ Browser Login\n'));
|
|
42
|
+
info(`Opening dashboard: ${chalk.cyan(settingsUrl)}`);
|
|
43
|
+
console.log(chalk.dim(' 1. Open the API Keys tab'));
|
|
44
|
+
console.log(chalk.dim(' 2. Create or copy an existing key'));
|
|
45
|
+
console.log(chalk.dim(' 3. Paste it below\n'));
|
|
46
|
+
|
|
47
|
+
const opened = openBrowser(settingsUrl);
|
|
48
|
+
if (!opened) {
|
|
49
|
+
warn('Could not open browser automatically.');
|
|
50
|
+
console.log(` Open manually: ${chalk.cyan(settingsUrl)}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { apiKey } = await prompts(
|
|
54
|
+
{
|
|
55
|
+
type: 'password',
|
|
56
|
+
name: 'apiKey',
|
|
57
|
+
message: 'Paste your API key here',
|
|
58
|
+
validate: (v) => (v && v.length > 10) ? true : 'Enter a valid API key',
|
|
59
|
+
},
|
|
60
|
+
{ onCancel: () => { error('Cancelled.'); process.exit(0); } }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (!apiKey) return;
|
|
64
|
+
|
|
65
|
+
// Quick verify the key works
|
|
66
|
+
const sp = spinner('Verifying key…').start();
|
|
67
|
+
try {
|
|
68
|
+
// Store temporarily to use the authenticated client
|
|
69
|
+
const cfg = load();
|
|
70
|
+
cfg.api_key = apiKey;
|
|
71
|
+
save(cfg);
|
|
72
|
+
|
|
73
|
+
await client.get('/auth/me');
|
|
74
|
+
sp.succeed(chalk.dim('Verified'));
|
|
75
|
+
success(`Logged in! Key stored at ${chalk.dim(CONFIG_FILE)}`);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Restore old key if verification fails
|
|
78
|
+
const cfg = load();
|
|
79
|
+
if (cfg.api_key === apiKey) delete cfg.api_key;
|
|
80
|
+
save(cfg);
|
|
81
|
+
sp.fail(chalk.red('Invalid key'));
|
|
82
|
+
error(e.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── email+password flow ───────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function apiFlow() {
|
|
89
|
+
console.log('\n' + sendcraftGradient(' ✦ Sign In\n'));
|
|
90
|
+
|
|
91
|
+
const answers = await prompts(
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
name: 'email',
|
|
96
|
+
message: 'Email address',
|
|
97
|
+
validate: (v) => v && v.includes('@') ? true : 'Enter a valid email',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'password',
|
|
101
|
+
name: 'password',
|
|
102
|
+
message: 'Password',
|
|
103
|
+
validate: (v) => v && v.length >= 4 ? true : 'Enter your password',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
{ onCancel: () => { error('Cancelled.'); process.exit(0); } }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!answers.email || !answers.password) return;
|
|
110
|
+
|
|
111
|
+
const sp = spinner('Signing in…').start();
|
|
112
|
+
let token, userName;
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.postNoAuth('/auth/login', {
|
|
115
|
+
email: answers.email,
|
|
116
|
+
password: answers.password,
|
|
117
|
+
});
|
|
118
|
+
token = result.token;
|
|
119
|
+
userName = result.user?.name || answers.email;
|
|
120
|
+
sp.succeed(chalk.dim('Authenticated'));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
sp.fail(chalk.red('Login failed'));
|
|
123
|
+
error(e.message === 'Invalid credentials'
|
|
124
|
+
? 'Wrong email or password. Try again.'
|
|
125
|
+
: e.message);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Auto-create a CLI API key using the JWT
|
|
130
|
+
const sp2 = spinner('Creating CLI API key…').start();
|
|
131
|
+
try {
|
|
132
|
+
const result = await client.postWithToken('/user/keys', {
|
|
133
|
+
name: 'CLI',
|
|
134
|
+
permissions: 'full_access',
|
|
135
|
+
}, token);
|
|
136
|
+
|
|
137
|
+
const apiKey = result.key?.key;
|
|
138
|
+
if (!apiKey) throw new Error('No key returned from server');
|
|
139
|
+
|
|
140
|
+
sp2.succeed(chalk.dim('API key created'));
|
|
141
|
+
|
|
142
|
+
const cfg = load();
|
|
143
|
+
cfg.api_key = apiKey;
|
|
144
|
+
save(cfg);
|
|
145
|
+
|
|
146
|
+
console.log('\n ' + chalk.bold.green(`Welcome, ${userName}!`));
|
|
147
|
+
success(`Logged in! Key stored at ${chalk.dim(CONFIG_FILE)}`);
|
|
148
|
+
console.log(
|
|
149
|
+
'\n ' + chalk.dim('Tip: run ') + chalk.cyan('sendcraft config show') +
|
|
150
|
+
chalk.dim(' to confirm your setup.\n')
|
|
151
|
+
);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
sp2.fail(chalk.red('Could not create API key'));
|
|
154
|
+
error(e.message);
|
|
155
|
+
info(`You can create a key manually at ${chalk.cyan(getWebUrl() + '/dashboard/settings')}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── command ──────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
const cmd = new Command('login')
|
|
162
|
+
.description('Log in to SendCraft and save your API key')
|
|
163
|
+
.option('--browser', 'Open dashboard in browser to copy an API key')
|
|
164
|
+
.option('--api', 'Log in with email and password via the API')
|
|
165
|
+
.action(async (opts) => {
|
|
166
|
+
if (opts.browser) return browserFlow();
|
|
167
|
+
if (opts.api) return apiFlow();
|
|
168
|
+
return runInteractive();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** Exported so the interactive TUI can call it directly */
|
|
172
|
+
async function runInteractive() {
|
|
173
|
+
console.log('\n' + sendcraftGradient(' ✦ SendCraft Login\n'));
|
|
174
|
+
|
|
175
|
+
const { method } = await prompts(
|
|
176
|
+
{
|
|
177
|
+
type: 'select',
|
|
178
|
+
name: 'method',
|
|
179
|
+
message: 'How would you like to log in?',
|
|
180
|
+
choices: [
|
|
181
|
+
{
|
|
182
|
+
title: '🌐 ' + chalk.bold('Browser') +
|
|
183
|
+
chalk.dim(' — open dashboard, copy your API key'),
|
|
184
|
+
value: 'browser',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: '🔐 ' + chalk.bold('Email & Password') +
|
|
188
|
+
chalk.dim(' — sign in and auto-generate a CLI key'),
|
|
189
|
+
value: 'api',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
title: chalk.dim('✕ Back'),
|
|
193
|
+
value: 'cancel',
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{ onCancel: () => null }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!method || method === 'cancel') return;
|
|
201
|
+
if (method === 'browser') return browserFlow();
|
|
202
|
+
if (method === 'api') return apiFlow();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
cmd.runInteractive = runInteractive;
|
|
206
|
+
module.exports = cmd;
|
package/lib/config.js
CHANGED
|
@@ -30,4 +30,24 @@ function getBaseUrl() {
|
|
|
30
30
|
return process.env.SENDCRAFT_BASE_URL || load().base_url || 'https://api.sendcraft.online/api';
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Derives the web dashboard URL from the API base URL.
|
|
35
|
+
* e.g. "https://api.sendcraft.online/api" → "https://sendcraft.online"
|
|
36
|
+
* "http://localhost:3000/api" → "http://localhost:3000"
|
|
37
|
+
* "https://myinstance.com/api" → "https://myinstance.com"
|
|
38
|
+
*/
|
|
39
|
+
function getWebUrl() {
|
|
40
|
+
const stored = load().web_url;
|
|
41
|
+
if (stored) return stored;
|
|
42
|
+
try {
|
|
43
|
+
const { URL } = require('url');
|
|
44
|
+
const parsed = new URL(getBaseUrl());
|
|
45
|
+
// Strip leading "api." subdomain if present
|
|
46
|
+
const host = parsed.hostname.replace(/^api\./, '');
|
|
47
|
+
return `${parsed.protocol}//${host}`;
|
|
48
|
+
} catch {
|
|
49
|
+
return 'https://sendcraft.online';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { load, save, getApiKey, getBaseUrl, getWebUrl, CONFIG_FILE };
|
package/lib/interactive.js
CHANGED
|
@@ -23,6 +23,7 @@ const MAIN_MENU = [
|
|
|
23
23
|
{ title: '🔑 ' + chalk.bold('API Keys'), value: 'keys' },
|
|
24
24
|
{ title: '🔥 ' + chalk.bold('SMTP Warmup'), value: 'warmup' },
|
|
25
25
|
{ title: '🤖 ' + chalk.bold('MCP Setup'), value: 'mcp' },
|
|
26
|
+
{ title: '🔐 ' + chalk.bold('Login'), value: 'login' },
|
|
26
27
|
{ title: '⚙️ ' + chalk.bold('Config'), value: 'config' },
|
|
27
28
|
{ title: chalk.dim('✕ Exit'), value: 'exit' },
|
|
28
29
|
];
|
|
@@ -369,10 +370,10 @@ module.exports = async function interactive() {
|
|
|
369
370
|
console.log(chalk.yellow(' ⚠ No API key configured yet.\n'));
|
|
370
371
|
const { setup } = await prompts({
|
|
371
372
|
type: 'confirm', name: 'setup',
|
|
372
|
-
message: '
|
|
373
|
+
message: 'Log in to SendCraft now?',
|
|
373
374
|
initial: true,
|
|
374
375
|
});
|
|
375
|
-
if (setup) await
|
|
376
|
+
if (setup) await require('./commands/login').runInteractive();
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
while (true) {
|
|
@@ -400,6 +401,7 @@ module.exports = async function interactive() {
|
|
|
400
401
|
if (choice === 'keys') await screenKeys();
|
|
401
402
|
if (choice === 'warmup') await screenWarmup();
|
|
402
403
|
if (choice === 'mcp') await screenMcp();
|
|
404
|
+
if (choice === 'login') await require('./commands/login').runInteractive();
|
|
403
405
|
if (choice === 'config') await screenConfig();
|
|
404
406
|
} catch (e) {
|
|
405
407
|
error('Unexpected error: ' + e.message);
|