@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 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} "Hello!"`));
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,5 @@
1
+ interface SendGroupOptions {
2
+ media?: string;
3
+ }
4
+ export declare function sendGroupCommand(numbers: string[], opts: SendGroupOptions): Promise<void>;
5
+ export {};
@@ -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
+ }
@@ -1 +1,5 @@
1
- export declare function sendCommand(number: string, message: string): Promise<void>;
1
+ interface SendOptions {
2
+ media?: string;
3
+ }
4
+ export declare function sendCommand(number: string, message: string, opts: SendOptions): Promise<void>;
5
+ export {};
@@ -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
- import { printError } from '../lib/format.js';
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}`));
@@ -1 +1,8 @@
1
- export declare function setupCommand(): Promise<void>;
1
+ interface SetupOptions {
2
+ email?: string;
3
+ code?: string;
4
+ company?: string;
5
+ contact?: string;
6
+ }
7
+ export declare function setupCommand(opts: SetupOptions): Promise<void>;
8
+ export {};
@@ -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
- const { overwrite } = await prompts({
30
- type: 'confirm',
31
- name: 'overwrite',
32
- message: `You already have an account configured (${existing.email}). Overwrite?`,
33
- initial: false
34
- }, { onCancel });
35
- if (!overwrite) {
36
- console.log(chalk.dim(' Setup cancelled.'));
37
- return;
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
- // Step 1: Collect email
41
- const { email } = await prompts({
42
- type: 'text',
43
- name: 'email',
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
- catch (err) {
54
- sendSpinner.fail(`Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
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
- console.log();
58
- // Step 3: Enter code
59
- const { code } = await prompts({
60
- type: 'text',
61
- name: 'code',
62
- message: 'Verification code',
63
- validate: (v) => /^\d{8}$/.test(v) || 'Enter the 8-digit code from your email'
64
- }, { onCancel });
65
- // Step 4: Company name (required for setup)
66
- const { companyName } = await prompts({
67
- type: 'text',
68
- name: 'companyName',
69
- message: 'Company name (lowercase, hyphens/underscores only)',
70
- validate: (v) => {
71
- if (!v)
72
- return 'Company name is required';
73
- if (!/^[a-z0-9_-]+$/.test(v))
74
- return 'Only lowercase letters, numbers, hyphens, and underscores';
75
- if (v.length < 3 || v.length > 64)
76
- return 'Must be 3-64 characters';
77
- return true;
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
- }, { onCancel });
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
- console.log(chalk.bold(' Add your first contact'));
104
- console.log(chalk.dim(' Enter the phone number you want to message via iMessage.'));
105
- console.log();
106
- const { contactNumber } = await prompts({
107
- type: 'text',
108
- name: 'contactNumber',
109
- message: 'Contact phone number',
110
- validate: (v) => {
111
- const n = normalizeNumber(v);
112
- return /^\+\d{10,15}$/.test(n) || 'Enter a valid phone number (e.g. +15551234567)';
113
- }
114
- }, { onCancel });
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} "Hello from Sendblue!"`));
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
- // Show QR code for easy texting
140
- const qr = await generateSmsQr(sharedNumber);
141
- console.log(chalk.dim(' Or scan this QR code to open a text:'));
142
- console.log();
143
- // Indent each line of the QR code
144
- for (const line of qr.split('\n')) {
145
- console.log(` ${line}`);
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} "Hello from Sendblue!"`));
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 = process.env.SENDBLUE_SETUP_URL || 'https://dashboard.sendblue.com';
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`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendblue/cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Sendblue CLI — iMessage numbers for agents",
5
5
  "type": "module",
6
6
  "bin": {