@sendblue/cli 0.2.0 → 0.4.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.
@@ -1,29 +1,27 @@
1
1
  import chalk from 'chalk';
2
+ import ora from 'ora';
2
3
  import { getCredentials } from '../lib/config.js';
3
4
  import { addContact, getSharedContacts } from '../lib/api.js';
4
- import { formatPhoneNumber, printError, printInfo, printSuccess } from '../lib/format.js';
5
+ import { formatPhoneNumber, normalizeNumber, printError } from '../lib/format.js';
5
6
  export async function addContactCommand(number) {
6
7
  const creds = getCredentials();
7
8
  if (!creds) {
8
9
  printError('No credentials found. Run `sendblue login` first.');
9
10
  process.exit(1);
10
11
  }
11
- // Normalize number
12
- const normalized = number.startsWith('+') ? number : `+${number}`;
13
- console.log();
14
- printInfo(` Adding contact ${formatPhoneNumber(normalized)}...`);
12
+ const normalized = normalizeNumber(number);
13
+ const spinner = ora({ text: `Adding contact ${formatPhoneNumber(normalized)}...`, indent: 2 }).start();
15
14
  try {
16
15
  const result = await addContact(creds.apiKey, creds.apiSecret, normalized);
17
16
  if (result.verified) {
18
- printSuccess(` Contact ${formatPhoneNumber(normalized)} is already verified!`);
17
+ spinner.succeed(`Contact ${formatPhoneNumber(normalized)} is already verified!`);
19
18
  console.log();
20
19
  console.log(chalk.bold(' You can now send messages:'));
21
20
  console.log(chalk.cyan(` sendblue send ${normalized} "Hello!"`));
22
21
  console.log();
23
22
  return;
24
23
  }
25
- // Contact added but needs verification
26
- console.log(chalk.green(` Contact added!`));
24
+ spinner.succeed('Contact added!');
27
25
  console.log();
28
26
  // Get the shared number
29
27
  const contacts = await getSharedContacts(creds.apiKey, creds.apiSecret);
@@ -46,7 +44,7 @@ export async function addContactCommand(number) {
46
44
  console.log();
47
45
  }
48
46
  catch (err) {
49
- printError(` Failed: ${err instanceof Error ? err.message : String(err)}`);
47
+ spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
50
48
  process.exit(1);
51
49
  }
52
50
  }
@@ -56,8 +54,10 @@ export async function contactsCommand() {
56
54
  printError('No credentials found. Run `sendblue login` first.');
57
55
  process.exit(1);
58
56
  }
57
+ const spinner = ora({ text: 'Fetching contacts...', indent: 2 }).start();
59
58
  try {
60
59
  const result = await getSharedContacts(creds.apiKey, creds.apiSecret);
60
+ spinner.stop();
61
61
  console.log();
62
62
  console.log(chalk.bold(' Contacts'));
63
63
  if (result.sharedNumber) {
@@ -84,7 +84,7 @@ export async function contactsCommand() {
84
84
  console.log();
85
85
  }
86
86
  catch (err) {
87
- printError(` Failed: ${err instanceof Error ? err.message : String(err)}`);
87
+ spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
88
88
  process.exit(1);
89
89
  }
90
90
  }
@@ -0,0 +1 @@
1
+ export declare function loginCommand(): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getCredentials, saveCredentials, credentialsPath } from '../lib/config.js';
5
+ import { sendCode, verifyLogin } from '../lib/api.js';
6
+ import { printError, formatPhoneNumber } from '../lib/format.js';
7
+ const onCancel = () => {
8
+ console.log();
9
+ printError('Login cancelled.');
10
+ process.exit(0);
11
+ };
12
+ export async function loginCommand() {
13
+ console.log();
14
+ console.log(chalk.bold(' sendblue login'));
15
+ console.log(chalk.dim(' Log in to an existing Sendblue account'));
16
+ console.log();
17
+ // Check for existing credentials
18
+ const existing = getCredentials();
19
+ if (existing) {
20
+ const { overwrite } = await prompts({
21
+ type: 'confirm',
22
+ name: 'overwrite',
23
+ message: `You already have an account configured (${existing.email}). Overwrite?`,
24
+ initial: false
25
+ }, { onCancel });
26
+ if (!overwrite) {
27
+ console.log(chalk.dim(' Login cancelled.'));
28
+ return;
29
+ }
30
+ }
31
+ // Step 1: Collect email
32
+ const { email } = await prompts({
33
+ type: 'text',
34
+ name: 'email',
35
+ message: 'Email',
36
+ validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Enter a valid email'
37
+ }, { onCancel });
38
+ // Step 2: Send verification code
39
+ const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
40
+ try {
41
+ await sendCode(email);
42
+ sendSpinner.succeed(`Code sent to ${email}`);
43
+ }
44
+ catch (err) {
45
+ sendSpinner.fail(`Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
46
+ process.exit(1);
47
+ }
48
+ console.log();
49
+ // Step 3: Enter code
50
+ const { code } = await prompts({
51
+ type: 'text',
52
+ name: 'code',
53
+ message: 'Verification code',
54
+ validate: (v) => /^\d{8}$/.test(v) || 'Enter the 8-digit code from your email'
55
+ }, { onCancel });
56
+ // Step 4: Verify code + look up existing account
57
+ const loginSpinner = ora({ text: 'Logging in...', indent: 2 }).start();
58
+ try {
59
+ const result = await verifyLogin(email, code);
60
+ loginSpinner.succeed('Logged in!');
61
+ saveCredentials({
62
+ apiKey: result.apiKey,
63
+ apiSecret: result.apiSecret,
64
+ email: result.email,
65
+ assignedNumber: result.assignedNumber,
66
+ plan: result.plan,
67
+ createdAt: new Date().toISOString()
68
+ });
69
+ console.log();
70
+ console.log(` ${chalk.bold('Email')}: ${result.email}`);
71
+ console.log(` ${chalk.bold('Company')}: ${result.companyName}`);
72
+ if (result.assignedNumber) {
73
+ console.log(` ${chalk.bold('Phone Number')}: ${formatPhoneNumber(result.assignedNumber)}`);
74
+ }
75
+ console.log(` ${chalk.bold('Plan')}: ${result.plan}`);
76
+ console.log();
77
+ console.log(chalk.dim(` Credentials saved to ${credentialsPath()}`));
78
+ console.log();
79
+ }
80
+ catch (err) {
81
+ loginSpinner.fail(`Login failed: ${err instanceof Error ? err.message : String(err)}`);
82
+ process.exit(1);
83
+ }
84
+ }
@@ -1,22 +1,26 @@
1
1
  import chalk from 'chalk';
2
+ import ora from 'ora';
2
3
  import { getCredentials } from '../lib/config.js';
3
4
  import { sendMessage } from '../lib/api.js';
4
- import { printError, printSuccess } from '../lib/format.js';
5
+ import { normalizeNumber } from '../lib/format.js';
6
+ import { printError } from '../lib/format.js';
5
7
  export async function sendCommand(number, message) {
6
8
  const creds = getCredentials();
7
9
  if (!creds) {
8
- printError('No credentials found. Run `sendblue setup` first.');
10
+ printError('No credentials found. Run `sendblue login` first.');
9
11
  process.exit(1);
10
12
  }
13
+ const normalized = normalizeNumber(number);
14
+ const spinner = ora({ text: `Sending to ${normalized}...`, indent: 2 }).start();
11
15
  try {
12
- const result = await sendMessage(creds.apiKey, creds.apiSecret, number, message);
13
- printSuccess(`Message sent to ${number}`);
16
+ const result = await sendMessage(creds.apiKey, creds.apiSecret, normalized, message);
17
+ spinner.succeed(`Message sent to ${normalized}`);
14
18
  if (result.messageId) {
15
19
  console.log(chalk.dim(` Message ID: ${result.messageId}`));
16
20
  }
17
21
  }
18
22
  catch (err) {
19
- printError(`Send failed: ${err instanceof Error ? err.message : String(err)}`);
23
+ spinner.fail(`Send failed: ${err instanceof Error ? err.message : String(err)}`);
20
24
  process.exit(1);
21
25
  }
22
26
  }
@@ -1 +1 @@
1
- export declare function loginCommand(): Promise<void>;
1
+ export declare function setupCommand(): Promise<void>;
@@ -1,12 +1,18 @@
1
1
  import prompts from 'prompts';
2
2
  import chalk from 'chalk';
3
+ import ora from 'ora';
3
4
  import { getCredentials, saveCredentials, credentialsPath } from '../lib/config.js';
4
- import { sendCode, verifyCode } from '../lib/api.js';
5
- import { printCredentials, printError, printInfo } from '../lib/format.js';
6
- export async function loginCommand() {
5
+ import { sendCode, verifySetup } from '../lib/api.js';
6
+ import { printCredentials, printError, formatPhoneNumber } from '../lib/format.js';
7
+ const onCancel = () => {
7
8
  console.log();
8
- console.log(chalk.bold(' sendblue login'));
9
- console.log(chalk.dim(' iMessage numbers for agents'));
9
+ printError('Setup cancelled.');
10
+ process.exit(0);
11
+ };
12
+ export async function setupCommand() {
13
+ console.log();
14
+ console.log(chalk.bold(' sendblue setup'));
15
+ console.log(chalk.dim(' Create a new Sendblue account'));
10
16
  console.log();
11
17
  // Check for existing credentials
12
18
  const existing = getCredentials();
@@ -16,9 +22,9 @@ export async function loginCommand() {
16
22
  name: 'overwrite',
17
23
  message: `You already have an account configured (${existing.email}). Overwrite?`,
18
24
  initial: false
19
- });
25
+ }, { onCancel });
20
26
  if (!overwrite) {
21
- printInfo('Login cancelled.');
27
+ console.log(chalk.dim(' Setup cancelled.'));
22
28
  return;
23
29
  }
24
30
  }
@@ -28,46 +34,45 @@ export async function loginCommand() {
28
34
  name: 'email',
29
35
  message: 'Email',
30
36
  validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Enter a valid email'
31
- });
32
- if (!email) {
33
- printError('Login cancelled.');
34
- return;
35
- }
37
+ }, { onCancel });
36
38
  // Step 2: Send verification code
37
- console.log();
38
- printInfo(' Sending verification code...');
39
+ const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
39
40
  try {
40
41
  await sendCode(email);
42
+ sendSpinner.succeed(`Code sent to ${email}`);
41
43
  }
42
44
  catch (err) {
43
- printError(` Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
45
+ sendSpinner.fail(`Failed to send code: ${err instanceof Error ? err.message : String(err)}`);
44
46
  process.exit(1);
45
47
  }
46
- console.log(chalk.green(` Code sent to ${email}`));
47
48
  console.log();
48
49
  // Step 3: Enter code
49
50
  const { code } = await prompts({
50
51
  type: 'text',
51
52
  name: 'code',
52
53
  message: 'Verification code',
53
- validate: (v) => /^\d{6}$/.test(v) || 'Enter the 6-digit code from your email'
54
- });
55
- if (!code) {
56
- printError('Login cancelled.');
57
- return;
58
- }
59
- // Step 4: Optional company name
54
+ validate: (v) => /^\d{8}$/.test(v) || 'Enter the 8-digit code from your email'
55
+ }, { onCancel });
56
+ // Step 4: Company name (required for setup)
60
57
  const { companyName } = await prompts({
61
58
  type: 'text',
62
59
  name: 'companyName',
63
- message: 'Company name (optional)',
64
- initial: ''
65
- });
66
- console.log();
67
- printInfo(' Setting up your account...');
60
+ message: 'Company name (lowercase, hyphens/underscores only)',
61
+ validate: (v) => {
62
+ if (!v)
63
+ return 'Company name is required';
64
+ if (!/^[a-z0-9_-]+$/.test(v))
65
+ return 'Only lowercase letters, numbers, hyphens, and underscores';
66
+ if (v.length < 3 || v.length > 64)
67
+ return 'Must be 3-64 characters';
68
+ return true;
69
+ }
70
+ }, { onCancel });
68
71
  // Step 5: Verify code + create account
72
+ const setupSpinner = ora({ text: 'Setting up your account...', indent: 2 }).start();
69
73
  try {
70
- const result = await verifyCode(email, code, companyName || undefined);
74
+ const result = await verifySetup(email, code, companyName);
75
+ setupSpinner.succeed('Account created!');
71
76
  saveCredentials({
72
77
  apiKey: result.apiKey,
73
78
  apiSecret: result.apiSecret,
@@ -79,12 +84,16 @@ export async function loginCommand() {
79
84
  printCredentials(result);
80
85
  console.log(chalk.dim(` Credentials saved to ${credentialsPath()}`));
81
86
  console.log();
82
- console.log(chalk.bold(' Quick start:'));
83
- console.log(chalk.cyan(` sendblue send +15551234567 "Hello from Sendblue!"`));
87
+ console.log(chalk.bold(' Next step — add a contact:'));
88
+ console.log(chalk.cyan(` sendblue add-contact +15551234567`));
84
89
  console.log();
90
+ if (result.assignedNumber) {
91
+ console.log(chalk.dim(` Your contact will need to text ${formatPhoneNumber(result.assignedNumber)} to verify.`));
92
+ console.log();
93
+ }
85
94
  }
86
95
  catch (err) {
87
- printError(` Setup failed: ${err instanceof Error ? err.message : String(err)}`);
96
+ setupSpinner.fail(`Setup failed: ${err instanceof Error ? err.message : String(err)}`);
88
97
  process.exit(1);
89
98
  }
90
99
  }
@@ -1,15 +1,18 @@
1
1
  import chalk from 'chalk';
2
+ import ora from 'ora';
2
3
  import { getCredentials } from '../lib/config.js';
3
4
  import { getAccount } from '../lib/api.js';
4
5
  import { formatPhoneNumber, printError } from '../lib/format.js';
5
6
  export async function statusCommand() {
6
7
  const creds = getCredentials();
7
8
  if (!creds) {
8
- printError('No credentials found. Run `sendblue setup` first.');
9
+ printError('No credentials found. Run `sendblue login` first.');
9
10
  process.exit(1);
10
11
  }
12
+ const spinner = ora({ text: 'Fetching account status...', indent: 2 }).start();
11
13
  try {
12
14
  const account = await getAccount(creds.apiKey, creds.apiSecret);
15
+ spinner.stop();
13
16
  console.log();
14
17
  console.log(chalk.bold(' Account Status'));
15
18
  console.log();
@@ -19,7 +22,7 @@ export async function statusCommand() {
19
22
  console.log();
20
23
  }
21
24
  catch (err) {
22
- printError(`Failed to fetch status: ${err instanceof Error ? err.message : String(err)}`);
25
+ spinner.fail(`Failed to fetch status: ${err instanceof Error ? err.message : String(err)}`);
23
26
  process.exit(1);
24
27
  }
25
28
  }
@@ -1,14 +1,23 @@
1
1
  import chalk from 'chalk';
2
+ import ora from 'ora';
2
3
  import { getCredentials, credentialsPath } from '../lib/config.js';
3
4
  import { testKeys } from '../lib/api.js';
4
5
  import { formatPhoneNumber, printError } from '../lib/format.js';
5
6
  export async function whoamiCommand() {
6
7
  const creds = getCredentials();
7
8
  if (!creds) {
8
- printError('No credentials found. Run `sendblue setup` first.');
9
+ printError('No credentials found. Run `sendblue login` first.');
9
10
  process.exit(1);
10
11
  }
11
- const valid = await testKeys(creds.apiKey, creds.apiSecret);
12
+ const spinner = ora({ text: 'Validating keys...', indent: 2 }).start();
13
+ let valid = false;
14
+ try {
15
+ valid = await testKeys(creds.apiKey, creds.apiSecret);
16
+ }
17
+ catch {
18
+ // Network error — keys may still be valid
19
+ }
20
+ spinner.stop();
12
21
  console.log();
13
22
  console.log(chalk.bold(' Current Account'));
14
23
  console.log();
package/dist/index.js CHANGED
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import { Command } from 'commander';
3
- import { loginCommand } from './commands/setup.js';
4
+ import { setupCommand } from './commands/setup.js';
5
+ import { loginCommand } from './commands/login.js';
4
6
  import { sendCommand } from './commands/send.js';
5
7
  import { statusCommand } from './commands/status.js';
6
8
  import { whoamiCommand } from './commands/whoami.js';
7
9
  import { addContactCommand, contactsCommand } from './commands/add-contact.js';
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require('../package.json');
8
12
  const program = new Command();
9
13
  program
10
14
  .name('sendblue')
11
15
  .description('Sendblue CLI — iMessage numbers for agents')
12
- .version('0.1.0');
16
+ .version(version);
17
+ program
18
+ .command('setup')
19
+ .description('Create a new Sendblue account and get an iMessage number')
20
+ .action(setupCommand);
13
21
  program
14
22
  .command('login')
15
- .description('Create an account and get an iMessage number')
23
+ .description('Log in to an existing Sendblue account')
16
24
  .action(loginCommand);
17
25
  program
18
26
  .command('add-contact')
package/dist/lib/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  interface SetupResponse {
2
2
  status: string;
3
3
  email: string;
4
+ companyName: string;
4
5
  apiKey: string;
5
6
  apiSecret: string;
6
7
  assignedNumber: string;
@@ -19,7 +20,8 @@ interface AccountResponse {
19
20
  [key: string]: unknown;
20
21
  }
21
22
  export declare function sendCode(email: string): Promise<void>;
22
- export declare function verifyCode(email: string, code: string, companyName?: string): Promise<SetupResponse>;
23
+ export declare function verifySetup(email: string, code: string, companyName: string): Promise<SetupResponse>;
24
+ export declare function verifyLogin(email: string, code: string): Promise<SetupResponse>;
23
25
  export declare function sendMessage(apiKey: string, apiSecret: string, number: string, content: string): Promise<SendMessageResponse>;
24
26
  export declare function getAccount(apiKey: string, apiSecret: string): Promise<AccountResponse>;
25
27
  interface ContactRoute {
package/dist/lib/api.js CHANGED
@@ -11,15 +11,27 @@ export async function sendCode(email) {
11
11
  throw new Error(body.error || body.message || `Failed to send code (${res.status})`);
12
12
  }
13
13
  }
14
- export async function verifyCode(email, code, companyName) {
14
+ export async function verifySetup(email, code, companyName) {
15
15
  const res = await fetch(`${SETUP_BASE}/api/v3/cli/setup`, {
16
16
  method: 'POST',
17
17
  headers: { 'Content-Type': 'application/json' },
18
- body: JSON.stringify({ email, code, companyName, action: 'verify' })
18
+ body: JSON.stringify({ email, code, companyName, action: 'verify-setup' })
19
19
  });
20
20
  if (!res.ok) {
21
21
  const body = await res.json().catch(() => ({}));
22
- throw new Error(body.error || body.message || `Verification failed (${res.status})`);
22
+ throw new Error(body.error || body.message || `Setup failed (${res.status})`);
23
+ }
24
+ return res.json();
25
+ }
26
+ export async function verifyLogin(email, code) {
27
+ const res = await fetch(`${SETUP_BASE}/api/v3/cli/setup`, {
28
+ method: 'POST',
29
+ headers: { 'Content-Type': 'application/json' },
30
+ body: JSON.stringify({ email, code, action: 'verify-login' })
31
+ });
32
+ if (!res.ok) {
33
+ const body = await res.json().catch(() => ({}));
34
+ throw new Error(body.error || body.message || `Login failed (${res.status})`);
23
35
  }
24
36
  return res.json();
25
37
  }
@@ -1,3 +1,4 @@
1
+ export declare function normalizeNumber(input: string): string;
1
2
  export declare function formatPhoneNumber(e164: string): string;
2
3
  export declare function printSuccess(message: string): void;
3
4
  export declare function printError(message: string): void;
@@ -1,4 +1,21 @@
1
1
  import chalk from 'chalk';
2
+ export function normalizeNumber(input) {
3
+ // Strip non-digit chars except leading +
4
+ let num = input.replace(/[^\d+]/g, '');
5
+ // 10-digit US number: prepend +1
6
+ if (/^\d{10}$/.test(num)) {
7
+ num = `+1${num}`;
8
+ }
9
+ // 11-digit starting with 1: prepend +
10
+ else if (/^1\d{10}$/.test(num)) {
11
+ num = `+${num}`;
12
+ }
13
+ // Already has +: keep as-is
14
+ else if (!num.startsWith('+')) {
15
+ num = `+${num}`;
16
+ }
17
+ return num;
18
+ }
2
19
  export function formatPhoneNumber(e164) {
3
20
  // +15551234567 -> +1 (555) 123-4567
4
21
  const match = e164.match(/^\+1(\d{3})(\d{3})(\d{4})$/);
@@ -22,7 +39,7 @@ export function printCredentials(data) {
22
39
  console.log();
23
40
  console.log(` ${chalk.bold('Phone Number')}: ${formatPhoneNumber(data.assignedNumber)}`);
24
41
  console.log(` ${chalk.bold('API Key')}: ${data.apiKey}`);
25
- console.log(` ${chalk.bold('API Secret')}: ${data.apiSecret}`);
42
+ console.log(` ${chalk.bold('API Secret')}: ${'*'.repeat(data.apiSecret.length - 4)}${data.apiSecret.slice(-4)}`);
26
43
  console.log(` ${chalk.bold('Plan')}: ${data.plan}`);
27
44
  console.log();
28
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendblue/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Sendblue CLI — iMessage numbers for agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "dependencies": {
30
30
  "chalk": "^5.3.0",
31
31
  "commander": "^12.1.0",
32
+ "ora": "^9.3.0",
32
33
  "prompts": "^2.4.2"
33
34
  },
34
35
  "devDependencies": {