@sendblue/cli 0.5.1 → 0.6.1

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.
@@ -34,7 +34,7 @@ export async function loginCommand() {
34
34
  type: 'text',
35
35
  name: 'email',
36
36
  message: 'Email',
37
- validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Enter a valid email'
37
+ validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
38
38
  }, { onCancel });
39
39
  // Step 2: Send verification code
40
40
  const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
@@ -0,0 +1,6 @@
1
+ export declare function messagesCommand(opts: {
2
+ number?: string;
3
+ limit?: string;
4
+ outbound?: boolean;
5
+ inbound?: boolean;
6
+ }): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getCredentials } from '../lib/config.js';
4
+ import { getMessages } from '../lib/api.js';
5
+ import { formatPhoneNumber, normalizeNumber, printError } from '../lib/format.js';
6
+ export async function messagesCommand(opts) {
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 messages...', indent: 2 }).start();
13
+ try {
14
+ const isOutbound = opts.outbound ? true : opts.inbound ? false : undefined;
15
+ const number = opts.number ? normalizeNumber(opts.number) : undefined;
16
+ const limit = Math.min(parseInt(opts.limit || '10', 10) || 10, 100);
17
+ const result = await getMessages(creds.apiKey, creds.apiSecret, {
18
+ number,
19
+ limit,
20
+ isOutbound
21
+ });
22
+ spinner.stop();
23
+ console.log();
24
+ if (result.data.length === 0) {
25
+ console.log(chalk.dim(' No messages found.'));
26
+ console.log();
27
+ return;
28
+ }
29
+ console.log(chalk.bold(` Messages`) + chalk.dim(` (${result.data.length} of ${result.pagination.total})`));
30
+ console.log();
31
+ for (const msg of result.data) {
32
+ const direction = msg.is_outbound
33
+ ? chalk.cyan('OUT')
34
+ : chalk.green(' IN');
35
+ const otherNumber = msg.is_outbound ? msg.to_number : msg.from_number;
36
+ const formatted = otherNumber && typeof otherNumber === 'string' ? formatPhoneNumber(otherNumber) : '?';
37
+ const date = new Date(msg.date_sent);
38
+ const timestamp = date.toLocaleString('en-US', {
39
+ month: 'short',
40
+ day: 'numeric',
41
+ hour: 'numeric',
42
+ minute: '2-digit',
43
+ hour12: true
44
+ });
45
+ const status = msg.status === 'SENT' || msg.status === 'DELIVERED'
46
+ ? ''
47
+ : ` ${chalk.yellow(`[${msg.status}]`)}`;
48
+ const content = msg.content
49
+ ? msg.content.length > 80
50
+ ? msg.content.slice(0, 77) + '...'
51
+ : msg.content
52
+ : chalk.dim('(media)');
53
+ console.log(` ${direction} ${chalk.dim(timestamp)} ${chalk.bold(formatted)}${status}`);
54
+ console.log(` ${content}`);
55
+ console.log();
56
+ }
57
+ if (result.pagination.hasMore) {
58
+ console.log(chalk.dim(` Showing ${result.data.length} of ${result.pagination.total}. Use --limit to see more.`));
59
+ console.log();
60
+ }
61
+ }
62
+ catch (err) {
63
+ spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
64
+ process.exit(1);
65
+ }
66
+ }
@@ -42,7 +42,7 @@ export async function setupCommand() {
42
42
  type: 'text',
43
43
  name: 'email',
44
44
  message: 'Email',
45
- validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || 'Enter a valid email'
45
+ validate: (v) => /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}$/.test(v) || 'Enter a valid email'
46
46
  }, { onCancel });
47
47
  // Step 2: Send verification code
48
48
  const sendSpinner = ora({ text: 'Sending verification code...', indent: 2 }).start();
package/dist/index.js CHANGED
@@ -4,15 +4,17 @@ 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 { messagesCommand } from './commands/messages.js';
7
8
  import { statusCommand } from './commands/status.js';
8
9
  import { whoamiCommand } from './commands/whoami.js';
9
10
  import { addContactCommand, contactsCommand } from './commands/add-contact.js';
11
+ import { getLogo } from './lib/format.js';
10
12
  const require = createRequire(import.meta.url);
11
13
  const { version } = require('../package.json');
12
14
  const program = new Command();
13
15
  program
14
16
  .name('sendblue')
15
- .description('Sendblue CLI — iMessage numbers for agents')
17
+ .description(getLogo())
16
18
  .version(version);
17
19
  program
18
20
  .command('setup')
@@ -37,6 +39,14 @@ program
37
39
  .argument('<number>', 'Recipient phone number (E.164 format)')
38
40
  .argument('<message>', 'Message content')
39
41
  .action(sendCommand);
42
+ program
43
+ .command('messages')
44
+ .description('View recent messages')
45
+ .option('-n, --number <number>', 'Filter by contact phone number')
46
+ .option('-l, --limit <count>', 'Number of messages to show', '10')
47
+ .option('--outbound', 'Show only outbound messages')
48
+ .option('--inbound', 'Show only inbound messages')
49
+ .action(messagesCommand);
40
50
  program
41
51
  .command('status')
42
52
  .description('Check your account status')
package/dist/lib/api.d.ts CHANGED
@@ -37,5 +37,35 @@ interface SharedContactsResponse {
37
37
  }
38
38
  export declare function addContact(apiKey: string, apiSecret: string, recipient: string): Promise<ContactRoute>;
39
39
  export declare function getSharedContacts(apiKey: string, apiSecret: string): Promise<SharedContactsResponse>;
40
+ export interface Message {
41
+ content: string;
42
+ number: string;
43
+ from_number: string;
44
+ to_number: string;
45
+ is_outbound: boolean;
46
+ status: string;
47
+ date_sent: string;
48
+ date_updated: string;
49
+ sendblue_number: string;
50
+ media_url?: string;
51
+ message_handle: string;
52
+ row_id: string;
53
+ [key: string]: unknown;
54
+ }
55
+ interface MessagesResponse {
56
+ status: string;
57
+ data: Message[];
58
+ pagination: {
59
+ total: number;
60
+ limit: number;
61
+ offset: number;
62
+ hasMore: boolean;
63
+ };
64
+ }
65
+ export declare function getMessages(apiKey: string, apiSecret: string, opts: {
66
+ number?: string;
67
+ limit?: number;
68
+ isOutbound?: boolean;
69
+ }): Promise<MessagesResponse>;
40
70
  export declare function testKeys(apiKey: string, apiSecret: string): Promise<boolean>;
41
71
  export {};
package/dist/lib/api.js CHANGED
@@ -99,6 +99,28 @@ export async function getSharedContacts(apiKey, apiSecret) {
99
99
  }
100
100
  return res.json();
101
101
  }
102
+ export async function getMessages(apiKey, apiSecret, opts) {
103
+ const params = new URLSearchParams();
104
+ params.set('limit', String(opts.limit || 10));
105
+ params.set('order_by', 'createdAt');
106
+ params.set('order_direction', 'desc');
107
+ if (opts.number)
108
+ params.set('number', opts.number);
109
+ if (opts.isOutbound !== undefined)
110
+ params.set('is_outbound', String(opts.isOutbound));
111
+ const res = await fetch(`${API_BASE}/api/v2/messages?${params}`, {
112
+ method: 'GET',
113
+ headers: {
114
+ 'sb-api-key-id': apiKey,
115
+ 'sb-api-secret-key': apiSecret
116
+ }
117
+ });
118
+ if (!res.ok) {
119
+ const body = await res.json().catch(() => ({}));
120
+ throw new Error(body.error || body.message || `Failed to get messages (${res.status})`);
121
+ }
122
+ return res.json();
123
+ }
102
124
  export async function testKeys(apiKey, apiSecret) {
103
125
  try {
104
126
  const res = await fetch(`${API_BASE}/account`, {
@@ -1,4 +1,5 @@
1
1
  export declare function printLogo(): void;
2
+ export declare function getLogo(): string;
2
3
  export declare function normalizeNumber(input: string): string;
3
4
  export declare function formatPhoneNumber(e164: string): string;
4
5
  export declare function printSuccess(message: string): void;
@@ -1,25 +1,106 @@
1
1
  import chalk from 'chalk';
2
2
  const blue = chalk.hex('#0088FF');
3
- export function printLogo() {
3
+ /*
4
+ export function printLogo(): void {
5
+ // Chat bubble with small satellite circle, matching Sendblue logo
6
+ const b = blue('█')
7
+ const h = blue('▀')
8
+ const l = blue('▄')
4
9
  const logo = [
5
- ` ${blue('██')} `,
6
- ` ${blue('██████')} `,
7
- ` ${blue('██████████')} `,
8
- ` ${blue('████████████')}`,
9
- ` ${blue('██████████████')}`,
10
- ` ${blue('██████████████')}`,
11
- ` ${blue('████████████')}`,
12
- ` ${blue('██████████')} `,
13
- ` ${blue('████████')} `,
14
- ];
10
+ ` ${l}${l}`,
11
+ ` ${l}${l}${l}${l}${l}${l}${l} ${l}${l}${l}`,
12
+ ` ${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
13
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
14
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${h}${h}${h}`,
15
+ ` ${l}${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
16
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
17
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
18
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
19
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
20
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
21
+ ` ${h}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${h}`,
22
+ ` ${h}${h}${h} ${h}${h}${h}${h}${h}${h}${h} ${h}${h}`,
23
+ ` ${h}${h}${h}${h}${h}`,
24
+ ]
15
25
  for (const line of logo) {
16
- console.log(` ${line}`);
26
+ console.log(` ${line}`)
27
+ }
28
+ console.log()
29
+ console.log(blue.bold(' sendblue'))
30
+ console.log(chalk.dim(' iMessage for agents'))
31
+ console.log()
32
+ }
33
+ */
34
+ export function printLogo() {
35
+ const lines = [
36
+ '███████╗███████╗███╗ ██╗██████╗',
37
+ '██╔════╝██╔════╝████╗ ██║██╔══██╗',
38
+ '███████╗█████╗ ██╔██╗ ██║██║ ██║',
39
+ '╚════██║██╔══╝ ██║╚██╗██║██║ ██║',
40
+ '███████║███████╗██║ ╚████║██████╔╝',
41
+ '╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝',
42
+ '',
43
+ '██████╗ ██╗ ██╗ ██╗███████╗',
44
+ '██╔══██╗██║ ██║ ██║██╔════╝',
45
+ '██████╔╝██║ ██║ ██║█████╗',
46
+ '██╔══██╗██║ ██║ ██║██╔══╝',
47
+ '██████╔╝███████╗╚██████╔╝███████╗',
48
+ '╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝',
49
+ ];
50
+ for (const line of lines) {
51
+ console.log(blue(line));
17
52
  }
18
53
  console.log();
19
- console.log(blue.bold(' sendblue'));
20
- console.log(chalk.dim(' iMessage for agents'));
54
+ console.log(chalk.dim(' iMessage for agents'));
21
55
  console.log();
22
56
  }
57
+ /*
58
+ export function getLogo(): string {
59
+ const b = blue('█')
60
+ const h = blue('▀')
61
+ const l = blue('▄')
62
+ const lines = [
63
+ ` ${l}${l}`,
64
+ ` ${l}${l}${l}${l}${l}${l}${l} ${l}${l}${l}`,
65
+ ` ${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
66
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${b}${b}${b}${b}${b}${b}`,
67
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b} ${h}${h}${h}`,
68
+ ` ${l}${l}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
69
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
70
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
71
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
72
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
73
+ ` ${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}`,
74
+ ` ${h}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${b}${h}`,
75
+ ` ${h}${h}${h} ${h}${h}${h}${h}${h}${h}${h} ${h}${h}`,
76
+ ` ${h}${h}${h}${h}${h}`,
77
+ ``,
78
+ blue.bold(' Sendblue'),
79
+ chalk.dim(' iMessage for agents'),
80
+ ]
81
+ return '\n' + lines.map(l => ` ${l}`).join('\n') + '\n'
82
+ }
83
+ */
84
+ export function getLogo() {
85
+ const lines = [
86
+ '███████╗███████╗███╗ ██╗██████╗',
87
+ '██╔════╝██╔════╝████╗ ██║██╔══██╗',
88
+ '███████╗█████╗ ██╔██╗ ██║██║ ██║',
89
+ '╚════██║██╔══╝ ██║╚██╗██║██║ ██║',
90
+ '███████║███████╗██║ ╚████║██████╔╝',
91
+ '╚══════╝╚══════╝╚═╝ ╚═══╝╚═════╝',
92
+ '',
93
+ '██████╗ ██╗ ██╗ ██╗███████╗',
94
+ '██╔══██╗██║ ██║ ██║██╔════╝',
95
+ '██████╔╝██║ ██║ ██║█████╗',
96
+ '██╔══██╗██║ ██║ ██║██╔══╝',
97
+ '██████╔╝███████╗╚██████╔╝███████╗',
98
+ '╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝',
99
+ '',
100
+ chalk.dim(' iMessage for agents'),
101
+ ];
102
+ return '\n' + lines.map(l => blue(l)).join('\n') + '\n';
103
+ }
23
104
  export function normalizeNumber(input) {
24
105
  // Strip non-digit chars except leading +
25
106
  let num = input.replace(/[^\d+]/g, '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendblue/cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Sendblue CLI — iMessage numbers for agents",
5
5
  "type": "module",
6
6
  "bin": {