@sendblue/cli 0.6.1 → 0.7.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 +105 -0
- package/dist/commands/add-contact.js +1 -1
- package/dist/commands/lines.d.ts +1 -0
- package/dist/commands/lines.js +35 -0
- package/dist/commands/send-group.d.ts +5 -0
- package/dist/commands/send-group.js +33 -0
- package/dist/commands/send.d.ts +5 -1
- package/dist/commands/send.js +3 -4
- package/dist/commands/setup.d.ts +8 -1
- package/dist/commands/setup.js +138 -68
- package/dist/commands/show-keys.d.ts +1 -0
- package/dist/commands/show-keys.js +16 -0
- package/dist/commands/typing.d.ts +1 -0
- package/dist/commands/typing.js +21 -0
- package/dist/commands/webhooks.d.ts +7 -0
- package/dist/commands/webhooks.js +78 -0
- package/dist/index.js +57 -9
- package/dist/lib/api.d.ts +17 -1
- package/dist/lib/api.js +104 -2
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Sendblue CLI
|
|
2
|
+
|
|
3
|
+
iMessage numbers for AI agents. Set up an iMessage-enabled phone number and start sending messages in under a minute.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @sendblue/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Create an account and get an iMessage number
|
|
17
|
+
sendblue setup
|
|
18
|
+
|
|
19
|
+
# Send a message
|
|
20
|
+
sendblue send +15551234567 'Hello from Sendblue!'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
### `sendblue setup`
|
|
26
|
+
|
|
27
|
+
Create a new Sendblue account. Walks you through email verification, company name, and adding your first contact.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Interactive (recommended for first time)
|
|
31
|
+
sendblue setup
|
|
32
|
+
|
|
33
|
+
# Non-interactive (for CI/scripts)
|
|
34
|
+
sendblue setup --email you@example.com # sends verification code, exits
|
|
35
|
+
sendblue setup --email you@example.com --code 12345678 --company my-co --contact +15551234567
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
|------|-------------|
|
|
40
|
+
| `--email <email>` | Email address |
|
|
41
|
+
| `--code <code>` | 8-digit verification code |
|
|
42
|
+
| `--company <name>` | Company name (lowercase, hyphens/underscores, 3-64 chars) |
|
|
43
|
+
| `--contact <number>` | First contact phone number (E.164 format) |
|
|
44
|
+
|
|
45
|
+
### `sendblue login`
|
|
46
|
+
|
|
47
|
+
Log in to an existing account.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
sendblue login
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `sendblue send <number> <message>`
|
|
54
|
+
|
|
55
|
+
Send an iMessage.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
sendblue send +15551234567 'Hey, your order shipped!'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `sendblue messages`
|
|
62
|
+
|
|
63
|
+
View recent messages.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
sendblue messages
|
|
67
|
+
sendblue messages -n +15551234567 --limit 20
|
|
68
|
+
sendblue messages --inbound
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Flag | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| `-n, --number <number>` | Filter by contact |
|
|
74
|
+
| `-l, --limit <count>` | Number of messages (default: 10) |
|
|
75
|
+
| `--outbound` | Show only sent messages |
|
|
76
|
+
| `--inbound` | Show only received messages |
|
|
77
|
+
|
|
78
|
+
### `sendblue add-contact <number>`
|
|
79
|
+
|
|
80
|
+
Add a contact to your account.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
sendblue add-contact +15551234567
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `sendblue contacts`
|
|
87
|
+
|
|
88
|
+
List all contacts and their verification status.
|
|
89
|
+
|
|
90
|
+
### `sendblue status`
|
|
91
|
+
|
|
92
|
+
Check your account status and plan.
|
|
93
|
+
|
|
94
|
+
### `sendblue whoami`
|
|
95
|
+
|
|
96
|
+
Show current credentials and verify they're valid.
|
|
97
|
+
|
|
98
|
+
## Credentials
|
|
99
|
+
|
|
100
|
+
Credentials are stored in `~/.sendblue/credentials.json` with `600` permissions (owner read/write only). Run `sendblue whoami` to see the current config path.
|
|
101
|
+
|
|
102
|
+
## Links
|
|
103
|
+
|
|
104
|
+
- [Sendblue](https://sendblue.co)
|
|
105
|
+
- [API Docs](https://docs.sendblue.co)
|
|
@@ -25,7 +25,7 @@ export async function addContactCommand(number) {
|
|
|
25
25
|
spinner.succeed(`Contact ${formatPhoneNumber(normalized)} is already verified!`);
|
|
26
26
|
console.log();
|
|
27
27
|
console.log(chalk.bold(' You can now send messages:'));
|
|
28
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
28
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello!'`));
|
|
29
29
|
console.log();
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function linesCommand(): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getCredentials } from '../lib/config.js';
|
|
4
|
+
import { getLines } from '../lib/api.js';
|
|
5
|
+
import { formatPhoneNumber, printError } from '../lib/format.js';
|
|
6
|
+
export async function linesCommand() {
|
|
7
|
+
const creds = getCredentials();
|
|
8
|
+
if (!creds) {
|
|
9
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const spinner = ora({ text: 'Fetching lines...', indent: 2 }).start();
|
|
13
|
+
try {
|
|
14
|
+
const result = await getLines(creds.apiKey, creds.apiSecret);
|
|
15
|
+
spinner.stop();
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold(' Phone Lines'));
|
|
18
|
+
console.log();
|
|
19
|
+
if (!result.numbers || result.numbers.length === 0) {
|
|
20
|
+
console.log(chalk.dim(' No lines assigned yet.'));
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
for (const number of result.numbers) {
|
|
24
|
+
console.log(` ${formatPhoneNumber(number)}`);
|
|
25
|
+
}
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(chalk.dim(` ${result.numbers.length} line${result.numbers.length === 1 ? '' : 's'} total`));
|
|
28
|
+
}
|
|
29
|
+
console.log();
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getCredentials } from '../lib/config.js';
|
|
4
|
+
import { sendGroupMessage } from '../lib/api.js';
|
|
5
|
+
import { normalizeNumber, formatPhoneNumber, printError } from '../lib/format.js';
|
|
6
|
+
export async function sendGroupCommand(numbers, opts) {
|
|
7
|
+
const creds = getCredentials();
|
|
8
|
+
if (!creds) {
|
|
9
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
// Last argument is the message, rest are numbers
|
|
13
|
+
if (numbers.length < 3) {
|
|
14
|
+
printError('Usage: sendblue send-group <number1> <number2> [number3...] <message>');
|
|
15
|
+
printError('At least 2 phone numbers and a message are required.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const message = numbers[numbers.length - 1];
|
|
19
|
+
const phoneNumbers = numbers.slice(0, -1).map(normalizeNumber);
|
|
20
|
+
const spinner = ora({ text: `Sending group message to ${phoneNumbers.length} recipients...`, indent: 2 }).start();
|
|
21
|
+
try {
|
|
22
|
+
const result = await sendGroupMessage(creds.apiKey, creds.apiSecret, phoneNumbers, message, creds.assignedNumber, opts.media);
|
|
23
|
+
spinner.succeed(`Group message sent to ${phoneNumbers.length} recipients`);
|
|
24
|
+
console.log(chalk.dim(` Recipients: ${phoneNumbers.map(formatPhoneNumber).join(', ')}`));
|
|
25
|
+
if (result.messageId) {
|
|
26
|
+
console.log(chalk.dim(` Message ID: ${result.messageId}`));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
spinner.fail(`Send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/commands/send.d.ts
CHANGED
package/dist/commands/send.js
CHANGED
|
@@ -2,9 +2,8 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { getCredentials } from '../lib/config.js';
|
|
4
4
|
import { sendMessage } from '../lib/api.js';
|
|
5
|
-
import { normalizeNumber } from '../lib/format.js';
|
|
6
|
-
|
|
7
|
-
export async function sendCommand(number, message) {
|
|
5
|
+
import { normalizeNumber, printError } from '../lib/format.js';
|
|
6
|
+
export async function sendCommand(number, message, opts) {
|
|
8
7
|
const creds = getCredentials();
|
|
9
8
|
if (!creds) {
|
|
10
9
|
printError('No credentials found. Run `sendblue login` first.');
|
|
@@ -13,7 +12,7 @@ export async function sendCommand(number, message) {
|
|
|
13
12
|
const normalized = normalizeNumber(number);
|
|
14
13
|
const spinner = ora({ text: `Sending to ${normalized}...`, indent: 2 }).start();
|
|
15
14
|
try {
|
|
16
|
-
const result = await sendMessage(creds.apiKey, creds.apiSecret, normalized, message, creds.assignedNumber);
|
|
15
|
+
const result = await sendMessage(creds.apiKey, creds.apiSecret, normalized, message, creds.assignedNumber, opts.media);
|
|
17
16
|
spinner.succeed(`Message sent to ${normalized}`);
|
|
18
17
|
if (result.messageId) {
|
|
19
18
|
console.log(chalk.dim(` Message ID: ${result.messageId}`));
|
package/dist/commands/setup.d.ts
CHANGED
package/dist/commands/setup.js
CHANGED
|
@@ -17,7 +17,8 @@ const onCancel = () => {
|
|
|
17
17
|
printError('Setup cancelled.');
|
|
18
18
|
process.exit(0);
|
|
19
19
|
};
|
|
20
|
-
export async function setupCommand() {
|
|
20
|
+
export async function setupCommand(opts) {
|
|
21
|
+
const nonInteractive = !!(opts.email && opts.code && opts.company);
|
|
21
22
|
console.log();
|
|
22
23
|
printLogo();
|
|
23
24
|
console.log(chalk.bold(' sendblue setup'));
|
|
@@ -26,57 +27,113 @@ export async function setupCommand() {
|
|
|
26
27
|
// Check for existing credentials
|
|
27
28
|
const existing = getCredentials();
|
|
28
29
|
if (existing) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
if (nonInteractive) {
|
|
31
|
+
console.log(chalk.dim(` Overwriting existing account (${existing.email})`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const { overwrite } = await prompts({
|
|
35
|
+
type: 'confirm',
|
|
36
|
+
name: 'overwrite',
|
|
37
|
+
message: `You already have an account configured (${existing.email}). Overwrite?`,
|
|
38
|
+
initial: false
|
|
39
|
+
}, { onCancel });
|
|
40
|
+
if (!overwrite) {
|
|
41
|
+
console.log(chalk.dim(' Setup cancelled.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
message: 'Email',
|
|
45
|
-
validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
|
|
46
|
-
}, { onCancel });
|
|
47
|
-
// Step 2: Send verification code
|
|
48
|
-
const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
|
|
49
|
-
try {
|
|
50
|
-
await sendCode(email);
|
|
51
|
-
sendSpinner.succeed(`Code sent to ${email}`);
|
|
46
|
+
// Validate flags upfront
|
|
47
|
+
if (opts.email && !/^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(opts.email)) {
|
|
48
|
+
printError('Invalid email address.');
|
|
49
|
+
process.exit(1);
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
if (opts.code && !/^\d{8}$/.test(opts.code)) {
|
|
52
|
+
printError('Verification code must be 8 digits.');
|
|
55
53
|
process.exit(1);
|
|
56
54
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
if (opts.company) {
|
|
56
|
+
if (!/^[a-z0-9_-]+$/.test(opts.company)) {
|
|
57
|
+
printError('Company name: only lowercase letters, numbers, hyphens, and underscores.');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (opts.company.length < 3 || opts.company.length > 64) {
|
|
61
|
+
printError('Company name must be 3-64 characters.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Step 1: Collect email
|
|
66
|
+
let email;
|
|
67
|
+
if (opts.email) {
|
|
68
|
+
email = opts.email;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const response = await prompts({
|
|
72
|
+
type: 'text',
|
|
73
|
+
name: 'email',
|
|
74
|
+
message: 'Email',
|
|
75
|
+
validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
|
|
76
|
+
}, { onCancel });
|
|
77
|
+
email = response.email;
|
|
78
|
+
}
|
|
79
|
+
// Step 2: Send verification code (skip if code already provided)
|
|
80
|
+
if (!opts.code) {
|
|
81
|
+
const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
|
|
82
|
+
try {
|
|
83
|
+
await sendCode(email);
|
|
84
|
+
sendSpinner.succeed(`Code sent to ${email}`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
sendSpinner.fail(`Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// Non-interactive: just send the code and exit so the user can
|
|
91
|
+
// re-run with --code once they have it
|
|
92
|
+
if (opts.email) {
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(chalk.dim(' Once you have the code, run:'));
|
|
95
|
+
console.log(chalk.cyan(` sendblue setup --email ${email} --code <CODE> --company <NAME>`));
|
|
96
|
+
console.log();
|
|
97
|
+
return;
|
|
78
98
|
}
|
|
79
|
-
|
|
99
|
+
console.log();
|
|
100
|
+
}
|
|
101
|
+
// Step 3: Enter code
|
|
102
|
+
let code;
|
|
103
|
+
if (opts.code) {
|
|
104
|
+
code = opts.code;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const response = await prompts({
|
|
108
|
+
type: 'text',
|
|
109
|
+
name: 'code',
|
|
110
|
+
message: 'Verification code',
|
|
111
|
+
validate: (v) => /^\d{8}$/.test(v) || 'Enter the 8-digit code from your email'
|
|
112
|
+
}, { onCancel });
|
|
113
|
+
code = response.code;
|
|
114
|
+
}
|
|
115
|
+
// Step 4: Company name
|
|
116
|
+
let companyName;
|
|
117
|
+
if (opts.company) {
|
|
118
|
+
companyName = opts.company;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const response = await prompts({
|
|
122
|
+
type: 'text',
|
|
123
|
+
name: 'companyName',
|
|
124
|
+
message: 'Company name (lowercase, hyphens/underscores only)',
|
|
125
|
+
validate: (v) => {
|
|
126
|
+
if (!v)
|
|
127
|
+
return 'Company name is required';
|
|
128
|
+
if (!/^[a-z0-9_-]+$/.test(v))
|
|
129
|
+
return 'Only lowercase letters, numbers, hyphens, and underscores';
|
|
130
|
+
if (v.length < 3 || v.length > 64)
|
|
131
|
+
return 'Must be 3-64 characters';
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}, { onCancel });
|
|
135
|
+
companyName = response.companyName;
|
|
136
|
+
}
|
|
80
137
|
// Step 5: Verify code + create account
|
|
81
138
|
const setupSpinner = ora({ text: 'Setting up your account...', indent: 2 }).start();
|
|
82
139
|
let result;
|
|
@@ -100,18 +157,29 @@ export async function setupCommand() {
|
|
|
100
157
|
process.exit(1);
|
|
101
158
|
}
|
|
102
159
|
// Step 6: Add first contact
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
160
|
+
let contactNumber;
|
|
161
|
+
if (opts.contact) {
|
|
162
|
+
contactNumber = opts.contact;
|
|
163
|
+
}
|
|
164
|
+
else if (!nonInteractive) {
|
|
165
|
+
console.log(chalk.bold(' Add your first contact'));
|
|
166
|
+
console.log(chalk.dim(' Enter the phone number you want to message via iMessage.'));
|
|
167
|
+
console.log();
|
|
168
|
+
const response = await prompts({
|
|
169
|
+
type: 'text',
|
|
170
|
+
name: 'contactNumber',
|
|
171
|
+
message: 'Contact phone number',
|
|
172
|
+
validate: (v) => {
|
|
173
|
+
const n = normalizeNumber(v);
|
|
174
|
+
return /^\+\d{10,15}$/.test(n) || 'Enter a valid phone number (e.g. +15551234567)';
|
|
175
|
+
}
|
|
176
|
+
}, { onCancel });
|
|
177
|
+
contactNumber = response.contactNumber;
|
|
178
|
+
}
|
|
179
|
+
if (!contactNumber) {
|
|
180
|
+
console.log();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
115
183
|
const normalized = normalizeNumber(contactNumber);
|
|
116
184
|
const contactSpinner = ora({ text: `Adding contact ${formatPhoneNumber(normalized)}...`, indent: 2 }).start();
|
|
117
185
|
try {
|
|
@@ -120,7 +188,7 @@ export async function setupCommand() {
|
|
|
120
188
|
contactSpinner.succeed(`Contact ${formatPhoneNumber(normalized)} is already verified!`);
|
|
121
189
|
console.log();
|
|
122
190
|
console.log(chalk.bold(' You\'re all set! Send a message:'));
|
|
123
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
191
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello from Sendblue!'`));
|
|
124
192
|
console.log();
|
|
125
193
|
return;
|
|
126
194
|
}
|
|
@@ -136,17 +204,19 @@ export async function setupCommand() {
|
|
|
136
204
|
console.log();
|
|
137
205
|
console.log(chalk.cyan.bold(` ${formatPhoneNumber(sharedNumber)}`));
|
|
138
206
|
console.log();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
if (!nonInteractive) {
|
|
208
|
+
// Show QR code for easy texting
|
|
209
|
+
const qr = await generateSmsQr(sharedNumber);
|
|
210
|
+
console.log(chalk.dim(' Or scan this QR code to open a text:'));
|
|
211
|
+
console.log();
|
|
212
|
+
// Indent each line of the QR code
|
|
213
|
+
for (const line of qr.split('\n')) {
|
|
214
|
+
console.log(` ${line}`);
|
|
215
|
+
}
|
|
216
|
+
console.log();
|
|
146
217
|
}
|
|
147
|
-
console.log();
|
|
148
218
|
console.log(chalk.dim(' Once they text in, run:'));
|
|
149
|
-
console.log(chalk.cyan(` sendblue send ${normalized}
|
|
219
|
+
console.log(chalk.cyan(` sendblue send ${normalized} 'Hello from Sendblue!'`));
|
|
150
220
|
}
|
|
151
221
|
console.log();
|
|
152
222
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function showKeysCommand(): Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getCredentials } from '../lib/config.js';
|
|
3
|
+
import { printError } from '../lib/format.js';
|
|
4
|
+
export async function showKeysCommand() {
|
|
5
|
+
const creds = getCredentials();
|
|
6
|
+
if (!creds) {
|
|
7
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(chalk.bold(' API Keys'));
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(` ${chalk.bold('API Key')}: ${creds.apiKey}`);
|
|
14
|
+
console.log(` ${chalk.bold('API Secret')}: ${creds.apiSecret}`);
|
|
15
|
+
console.log();
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function typingCommand(number: string): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { getCredentials } from '../lib/config.js';
|
|
3
|
+
import { sendTypingIndicator } from '../lib/api.js';
|
|
4
|
+
import { normalizeNumber, printError } from '../lib/format.js';
|
|
5
|
+
export async function typingCommand(number) {
|
|
6
|
+
const creds = getCredentials();
|
|
7
|
+
if (!creds) {
|
|
8
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const normalized = normalizeNumber(number);
|
|
12
|
+
const spinner = ora({ text: `Sending typing indicator to ${normalized}...`, indent: 2 }).start();
|
|
13
|
+
try {
|
|
14
|
+
await sendTypingIndicator(creds.apiKey, creds.apiSecret, normalized, creds.assignedNumber);
|
|
15
|
+
spinner.succeed(`Typing indicator sent to ${normalized}`);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function webhooksListCommand(): Promise<void>;
|
|
2
|
+
export declare function webhooksAddCommand(url: string, opts: {
|
|
3
|
+
type: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function webhooksRemoveCommand(url: string, opts: {
|
|
6
|
+
type: string;
|
|
7
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { getCredentials } from '../lib/config.js';
|
|
4
|
+
import { getWebhooks, addWebhook, deleteWebhook } from '../lib/api.js';
|
|
5
|
+
import { printError } from '../lib/format.js';
|
|
6
|
+
const VALID_TYPES = ['receive', 'outbound', 'call_log', 'line_blocked', 'line_assigned', 'contact_created'];
|
|
7
|
+
export async function webhooksListCommand() {
|
|
8
|
+
const creds = getCredentials();
|
|
9
|
+
if (!creds) {
|
|
10
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const spinner = ora({ text: 'Fetching webhooks...', indent: 2 }).start();
|
|
14
|
+
try {
|
|
15
|
+
const result = await getWebhooks(creds.apiKey, creds.apiSecret);
|
|
16
|
+
spinner.stop();
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(chalk.bold(' Webhooks'));
|
|
19
|
+
console.log();
|
|
20
|
+
if (!result.webhooks || result.webhooks.length === 0) {
|
|
21
|
+
console.log(chalk.dim(' No webhooks configured.'));
|
|
22
|
+
console.log();
|
|
23
|
+
console.log(chalk.dim(' Add one with:'));
|
|
24
|
+
console.log(chalk.cyan(' sendblue webhooks add <url> --type receive'));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const wh of result.webhooks) {
|
|
28
|
+
const type = wh.type ? chalk.dim(` (${wh.type})`) : '';
|
|
29
|
+
console.log(` ${chalk.cyan(wh.url)}${type}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function webhooksAddCommand(url, opts) {
|
|
40
|
+
const creds = getCredentials();
|
|
41
|
+
if (!creds) {
|
|
42
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
46
|
+
printError(`Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const spinner = ora({ text: 'Adding webhook...', indent: 2 }).start();
|
|
50
|
+
try {
|
|
51
|
+
await addWebhook(creds.apiKey, creds.apiSecret, url, opts.type);
|
|
52
|
+
spinner.succeed(`Webhook added: ${url} (${opts.type})`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function webhooksRemoveCommand(url, opts) {
|
|
60
|
+
const creds = getCredentials();
|
|
61
|
+
if (!creds) {
|
|
62
|
+
printError('No credentials found. Run `sendblue login` first.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
if (!VALID_TYPES.includes(opts.type)) {
|
|
66
|
+
printError(`Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const spinner = ora({ text: 'Removing webhook...', indent: 2 }).start();
|
|
70
|
+
try {
|
|
71
|
+
await deleteWebhook(creds.apiKey, creds.apiSecret, url, opts.type);
|
|
72
|
+
spinner.succeed(`Webhook removed: ${url} (${opts.type})`);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,15 @@ import { Command } from 'commander';
|
|
|
4
4
|
import { setupCommand } from './commands/setup.js';
|
|
5
5
|
import { loginCommand } from './commands/login.js';
|
|
6
6
|
import { sendCommand } from './commands/send.js';
|
|
7
|
+
import { sendGroupCommand } from './commands/send-group.js';
|
|
7
8
|
import { messagesCommand } from './commands/messages.js';
|
|
8
9
|
import { statusCommand } from './commands/status.js';
|
|
9
10
|
import { whoamiCommand } from './commands/whoami.js';
|
|
10
11
|
import { addContactCommand, contactsCommand } from './commands/add-contact.js';
|
|
12
|
+
import { typingCommand } from './commands/typing.js';
|
|
13
|
+
import { linesCommand } from './commands/lines.js';
|
|
14
|
+
import { webhooksListCommand, webhooksAddCommand, webhooksRemoveCommand } from './commands/webhooks.js';
|
|
15
|
+
import { showKeysCommand } from './commands/show-keys.js';
|
|
11
16
|
import { getLogo } from './lib/format.js';
|
|
12
17
|
const require = createRequire(import.meta.url);
|
|
13
18
|
const { version } = require('../package.json');
|
|
@@ -19,26 +24,33 @@ program
|
|
|
19
24
|
program
|
|
20
25
|
.command('setup')
|
|
21
26
|
.description('Create a new Sendblue account and get an iMessage number')
|
|
27
|
+
.option('--email <email>', 'Email address (skip prompt)')
|
|
28
|
+
.option('--code <code>', 'Verification code (skip prompt, requires --email)')
|
|
29
|
+
.option('--company <name>', 'Company name (skip prompt)')
|
|
30
|
+
.option('--contact <number>', 'First contact phone number (skip prompt)')
|
|
22
31
|
.action(setupCommand);
|
|
23
32
|
program
|
|
24
33
|
.command('login')
|
|
25
34
|
.description('Log in to an existing Sendblue account')
|
|
26
35
|
.action(loginCommand);
|
|
27
|
-
program
|
|
28
|
-
.command('add-contact')
|
|
29
|
-
.description('Add a contact to your account')
|
|
30
|
-
.argument('<number>', 'Contact phone number (E.164 format)')
|
|
31
|
-
.action(addContactCommand);
|
|
32
|
-
program
|
|
33
|
-
.command('contacts')
|
|
34
|
-
.description('List contacts and verification status')
|
|
35
|
-
.action(contactsCommand);
|
|
36
36
|
program
|
|
37
37
|
.command('send')
|
|
38
38
|
.description('Send a message')
|
|
39
39
|
.argument('<number>', 'Recipient phone number (E.164 format)')
|
|
40
40
|
.argument('<message>', 'Message content')
|
|
41
|
+
.option('--media <url>', 'Attach a media URL (image, video, etc.)')
|
|
41
42
|
.action(sendCommand);
|
|
43
|
+
program
|
|
44
|
+
.command('send-group')
|
|
45
|
+
.description('Send a group message')
|
|
46
|
+
.argument('<args...>', 'Phone numbers followed by message: <num1> <num2> [...] <message>')
|
|
47
|
+
.option('--media <url>', 'Attach a media URL')
|
|
48
|
+
.action(sendGroupCommand);
|
|
49
|
+
program
|
|
50
|
+
.command('typing')
|
|
51
|
+
.description('Send a typing indicator')
|
|
52
|
+
.argument('<number>', 'Recipient phone number (E.164 format)')
|
|
53
|
+
.action(typingCommand);
|
|
42
54
|
program
|
|
43
55
|
.command('messages')
|
|
44
56
|
.description('View recent messages')
|
|
@@ -47,6 +59,38 @@ program
|
|
|
47
59
|
.option('--outbound', 'Show only outbound messages')
|
|
48
60
|
.option('--inbound', 'Show only inbound messages')
|
|
49
61
|
.action(messagesCommand);
|
|
62
|
+
program
|
|
63
|
+
.command('add-contact')
|
|
64
|
+
.description('Add a contact to your account')
|
|
65
|
+
.argument('<number>', 'Contact phone number (E.164 format)')
|
|
66
|
+
.action(addContactCommand);
|
|
67
|
+
program
|
|
68
|
+
.command('contacts')
|
|
69
|
+
.description('List contacts and verification status')
|
|
70
|
+
.action(contactsCommand);
|
|
71
|
+
program
|
|
72
|
+
.command('lines')
|
|
73
|
+
.description('List phone numbers on your account')
|
|
74
|
+
.action(linesCommand);
|
|
75
|
+
const webhooks = program
|
|
76
|
+
.command('webhooks')
|
|
77
|
+
.description('Manage webhooks');
|
|
78
|
+
webhooks
|
|
79
|
+
.command('list')
|
|
80
|
+
.description('List configured webhooks')
|
|
81
|
+
.action(webhooksListCommand);
|
|
82
|
+
webhooks
|
|
83
|
+
.command('add')
|
|
84
|
+
.description('Add a webhook')
|
|
85
|
+
.argument('<url>', 'Webhook URL')
|
|
86
|
+
.requiredOption('--type <type>', 'Event type (receive, outbound, call_log, line_blocked, line_assigned, contact_created)')
|
|
87
|
+
.action(webhooksAddCommand);
|
|
88
|
+
webhooks
|
|
89
|
+
.command('remove')
|
|
90
|
+
.description('Remove a webhook')
|
|
91
|
+
.argument('<url>', 'Webhook URL')
|
|
92
|
+
.requiredOption('--type <type>', 'Event type')
|
|
93
|
+
.action(webhooksRemoveCommand);
|
|
50
94
|
program
|
|
51
95
|
.command('status')
|
|
52
96
|
.description('Check your account status')
|
|
@@ -55,4 +99,8 @@ program
|
|
|
55
99
|
.command('whoami')
|
|
56
100
|
.description('Show current credentials')
|
|
57
101
|
.action(whoamiCommand);
|
|
102
|
+
program
|
|
103
|
+
.command('show-keys')
|
|
104
|
+
.description('Show your API key and secret')
|
|
105
|
+
.action(showKeysCommand);
|
|
58
106
|
program.parse();
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ interface AccountResponse {
|
|
|
22
22
|
export declare function sendCode(email: string): Promise<void>;
|
|
23
23
|
export declare function verifySetup(email: string, code: string, companyName: string): Promise<SetupResponse>;
|
|
24
24
|
export declare function verifyLogin(email: string, code: string): Promise<SetupResponse>;
|
|
25
|
-
export declare function sendMessage(apiKey: string, apiSecret: string, number: string, content: string, fromNumber?: string): Promise<SendMessageResponse>;
|
|
25
|
+
export declare function sendMessage(apiKey: string, apiSecret: string, number: string, content: string, fromNumber?: string, mediaUrl?: string): Promise<SendMessageResponse>;
|
|
26
26
|
export declare function getAccount(apiKey: string, apiSecret: string): Promise<AccountResponse>;
|
|
27
27
|
interface ContactRoute {
|
|
28
28
|
id: number;
|
|
@@ -67,5 +67,21 @@ export declare function getMessages(apiKey: string, apiSecret: string, opts: {
|
|
|
67
67
|
limit?: number;
|
|
68
68
|
isOutbound?: boolean;
|
|
69
69
|
}): Promise<MessagesResponse>;
|
|
70
|
+
export declare function sendTypingIndicator(apiKey: string, apiSecret: string, number: string, fromNumber?: string): Promise<void>;
|
|
71
|
+
export declare function sendGroupMessage(apiKey: string, apiSecret: string, numbers: string[], content: string, fromNumber?: string, mediaUrl?: string): Promise<SendMessageResponse>;
|
|
72
|
+
export declare function getLines(apiKey: string, apiSecret: string): Promise<{
|
|
73
|
+
numbers: string[];
|
|
74
|
+
}>;
|
|
75
|
+
interface Webhook {
|
|
76
|
+
url: string;
|
|
77
|
+
type?: string;
|
|
78
|
+
}
|
|
79
|
+
interface WebhooksResponse {
|
|
80
|
+
webhooks: Webhook[];
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
export declare function getWebhooks(apiKey: string, apiSecret: string): Promise<WebhooksResponse>;
|
|
84
|
+
export declare function addWebhook(apiKey: string, apiSecret: string, url: string, type: string): Promise<void>;
|
|
85
|
+
export declare function deleteWebhook(apiKey: string, apiSecret: string, url: string, type: string): Promise<void>;
|
|
70
86
|
export declare function testKeys(apiKey: string, apiSecret: string): Promise<boolean>;
|
|
71
87
|
export {};
|
package/dist/lib/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const API_BASE = 'https://api.sendblue.com';
|
|
2
|
-
const SETUP_BASE =
|
|
2
|
+
const SETUP_BASE = 'https://dashboard.sendblue.com';
|
|
3
3
|
export async function sendCode(email) {
|
|
4
4
|
const res = await fetch(`${SETUP_BASE}/api/v3/cli/setup`, {
|
|
5
5
|
method: 'POST',
|
|
@@ -35,10 +35,12 @@ export async function verifyLogin(email, code) {
|
|
|
35
35
|
}
|
|
36
36
|
return res.json();
|
|
37
37
|
}
|
|
38
|
-
export async function sendMessage(apiKey, apiSecret, number, content, fromNumber) {
|
|
38
|
+
export async function sendMessage(apiKey, apiSecret, number, content, fromNumber, mediaUrl) {
|
|
39
39
|
const body = { number, content };
|
|
40
40
|
if (fromNumber)
|
|
41
41
|
body.from_number = fromNumber;
|
|
42
|
+
if (mediaUrl)
|
|
43
|
+
body.media_url = mediaUrl;
|
|
42
44
|
const res = await fetch(`${API_BASE}/api/send-message`, {
|
|
43
45
|
method: 'POST',
|
|
44
46
|
headers: {
|
|
@@ -121,6 +123,106 @@ export async function getMessages(apiKey, apiSecret, opts) {
|
|
|
121
123
|
}
|
|
122
124
|
return res.json();
|
|
123
125
|
}
|
|
126
|
+
// --- Typing indicator ---
|
|
127
|
+
export async function sendTypingIndicator(apiKey, apiSecret, number, fromNumber) {
|
|
128
|
+
const body = { number };
|
|
129
|
+
if (fromNumber)
|
|
130
|
+
body.from_number = fromNumber;
|
|
131
|
+
const res = await fetch(`${API_BASE}/api/send-typing-indicator`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'sb-api-key-id': apiKey,
|
|
136
|
+
'sb-api-secret-key': apiSecret
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify(body)
|
|
139
|
+
});
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const body = await res.json().catch(() => ({}));
|
|
142
|
+
throw new Error(body.error || body.message || `Failed to send typing indicator (${res.status})`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// --- Group messaging ---
|
|
146
|
+
export async function sendGroupMessage(apiKey, apiSecret, numbers, content, fromNumber, mediaUrl) {
|
|
147
|
+
const body = { numbers, content };
|
|
148
|
+
if (fromNumber)
|
|
149
|
+
body.from_number = fromNumber;
|
|
150
|
+
if (mediaUrl)
|
|
151
|
+
body.media_url = mediaUrl;
|
|
152
|
+
const res = await fetch(`${API_BASE}/api/send-group-message`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': 'application/json',
|
|
156
|
+
'sb-api-key-id': apiKey,
|
|
157
|
+
'sb-api-secret-key': apiSecret
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(body)
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const body = await res.json().catch(() => ({}));
|
|
163
|
+
throw new Error(body.error || body.message || `Failed to send group message (${res.status})`);
|
|
164
|
+
}
|
|
165
|
+
return res.json();
|
|
166
|
+
}
|
|
167
|
+
// --- Lines ---
|
|
168
|
+
export async function getLines(apiKey, apiSecret) {
|
|
169
|
+
const res = await fetch(`${API_BASE}/api/lines`, {
|
|
170
|
+
method: 'GET',
|
|
171
|
+
headers: {
|
|
172
|
+
'sb-api-key-id': apiKey,
|
|
173
|
+
'sb-api-secret-key': apiSecret
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const body = await res.json().catch(() => ({}));
|
|
178
|
+
throw new Error(body.error || body.message || `Failed to get lines (${res.status})`);
|
|
179
|
+
}
|
|
180
|
+
return res.json();
|
|
181
|
+
}
|
|
182
|
+
export async function getWebhooks(apiKey, apiSecret) {
|
|
183
|
+
const res = await fetch(`${API_BASE}/api/account/webhooks`, {
|
|
184
|
+
method: 'GET',
|
|
185
|
+
headers: {
|
|
186
|
+
'sb-api-key-id': apiKey,
|
|
187
|
+
'sb-api-secret-key': apiSecret
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const body = await res.json().catch(() => ({}));
|
|
192
|
+
throw new Error(body.error || body.message || `Failed to get webhooks (${res.status})`);
|
|
193
|
+
}
|
|
194
|
+
return res.json();
|
|
195
|
+
}
|
|
196
|
+
export async function addWebhook(apiKey, apiSecret, url, type) {
|
|
197
|
+
const res = await fetch(`${API_BASE}/api/account/webhooks`, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
'sb-api-key-id': apiKey,
|
|
202
|
+
'sb-api-secret-key': apiSecret
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ webhooks: [url], type })
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) {
|
|
207
|
+
const body = await res.json().catch(() => ({}));
|
|
208
|
+
throw new Error(body.error || body.message || `Failed to add webhook (${res.status})`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
export async function deleteWebhook(apiKey, apiSecret, url, type) {
|
|
212
|
+
const res = await fetch(`${API_BASE}/api/account/webhooks`, {
|
|
213
|
+
method: 'DELETE',
|
|
214
|
+
headers: {
|
|
215
|
+
'Content-Type': 'application/json',
|
|
216
|
+
'sb-api-key-id': apiKey,
|
|
217
|
+
'sb-api-secret-key': apiSecret
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({ webhooks: [url], type })
|
|
220
|
+
});
|
|
221
|
+
if (!res.ok) {
|
|
222
|
+
const body = await res.json().catch(() => ({}));
|
|
223
|
+
throw new Error(body.error || body.message || `Failed to delete webhook (${res.status})`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
124
226
|
export async function testKeys(apiKey, apiSecret) {
|
|
125
227
|
try {
|
|
126
228
|
const res = await fetch(`${API_BASE}/account`, {
|