@ktmcp-cli/telnyx 1.0.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/AGENT.md ADDED
@@ -0,0 +1,82 @@
1
+ # AGENT.md — Telnyx CLI for AI Agents
2
+
3
+ This document explains how to use the Telnyx CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `telnyx` CLI provides access to Telnyx's communications platform API. Use it to send SMS, make calls, manage phone numbers, and verify users.
8
+
9
+ ## Prerequisites
10
+
11
+ ```bash
12
+ telnyx config set --api-key <key>
13
+ ```
14
+
15
+ Get API key from: https://portal.telnyx.com/#/app/api-keys
16
+
17
+ ## All Commands
18
+
19
+ ### Config
20
+
21
+ ```bash
22
+ telnyx config set --api-key <key>
23
+ telnyx config show
24
+ ```
25
+
26
+ ### Messages
27
+
28
+ ```bash
29
+ telnyx messages send --from +1XXX --to +1YYY --text "Hello"
30
+ telnyx messages list
31
+ telnyx messages list --json
32
+ telnyx messages get <id>
33
+ ```
34
+
35
+ ### Numbers
36
+
37
+ ```bash
38
+ telnyx numbers list
39
+ telnyx numbers list --json
40
+ telnyx numbers get <id>
41
+ telnyx numbers search --country US
42
+ telnyx numbers order +12125551234
43
+ ```
44
+
45
+ ### Calls
46
+
47
+ ```bash
48
+ telnyx calls create --to +1YYY --from +1XXX
49
+ telnyx calls hangup <call-control-id>
50
+ telnyx calls speak <call-control-id> --text "Hello"
51
+ ```
52
+
53
+ ### Verify
54
+
55
+ ```bash
56
+ telnyx verify send --to +1XXX # Send SMS code
57
+ telnyx verify send --to +1XXX --type call # Voice call code
58
+ telnyx verify check <id> --code 123456 # Verify the code
59
+ telnyx verify list
60
+ ```
61
+
62
+ ### Connections
63
+
64
+ ```bash
65
+ telnyx connections list
66
+ telnyx connections get <id>
67
+ ```
68
+
69
+ ### Profiles
70
+
71
+ ```bash
72
+ telnyx profiles list
73
+ telnyx profiles get <id>
74
+ ```
75
+
76
+ ## Tips for Agents
77
+
78
+ 1. Always use `--json` when parsing results programmatically
79
+ 2. Phone numbers must be in E.164 format: `+12025551234`
80
+ 3. The `call-control-id` from `calls create` is used for call actions
81
+ 4. Verification IDs from `verify send` are needed for `verify check`
82
+ 5. Use `numbers search` before `numbers order` to find available numbers
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ ![Banner](https://raw.githubusercontent.com/ktmcp-cli/telnyx/main/banner.svg)
2
+
3
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
4
+ >
5
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
6
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
7
+
8
+ # Telnyx CLI
9
+
10
+ > **⚠️ Unofficial CLI** - Not officially sponsored or affiliated with Telnyx.
11
+
12
+ A production-ready command-line interface for the [Telnyx API](https://developers.telnyx.com/) — programmable communications platform. Send SMS, make calls, manage phone numbers, and verify users directly from your terminal.
13
+
14
+ ## Features
15
+
16
+ - **Messages** — Send and receive SMS/MMS messages
17
+ - **Phone Numbers** — List, search, and order phone numbers
18
+ - **Calls** — Initiate and control phone calls
19
+ - **Verify** — Send and check 2FA verification codes
20
+ - **Connections** — Manage SIP trunks and connections
21
+ - **Messaging Profiles** — Configure messaging settings
22
+ - **JSON output** — All commands support `--json` for scripting
23
+ - **Colorized output** — Clean terminal output with chalk
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install -g @ktmcp-cli/telnyx
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Get your API key from https://portal.telnyx.com/#/app/api-keys
35
+ telnyx config set --api-key YOUR_API_KEY
36
+
37
+ # Send an SMS
38
+ telnyx messages send --from +12025551234 --to +19175559876 --text "Hello from Telnyx CLI!"
39
+
40
+ # List your phone numbers
41
+ telnyx numbers list
42
+
43
+ # Search for available numbers
44
+ telnyx numbers search --country US --area-code 212
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ ### Config
50
+
51
+ ```bash
52
+ telnyx config set --api-key <key>
53
+ telnyx config show
54
+ ```
55
+
56
+ ### Messages
57
+
58
+ ```bash
59
+ telnyx messages send --from +1XXX --to +1YYY --text "Hello"
60
+ telnyx messages send --from +1XXX --to +1YYY --text "Photo" --media-urls https://example.com/photo.jpg
61
+ telnyx messages list
62
+ telnyx messages list --json
63
+ telnyx messages get <message-id>
64
+ ```
65
+
66
+ ### Phone Numbers
67
+
68
+ ```bash
69
+ telnyx numbers list
70
+ telnyx numbers list --json
71
+ telnyx numbers get <number-id>
72
+ telnyx numbers search --country US
73
+ telnyx numbers search --country US --area-code 212
74
+ telnyx numbers order +12125551234
75
+ telnyx numbers order +12125551234 --connection-id <id>
76
+ ```
77
+
78
+ ### Calls
79
+
80
+ ```bash
81
+ telnyx calls create --to +1YYY --from +1XXX
82
+ telnyx calls create --to +1YYY --from +1XXX --connection-id <id>
83
+ telnyx calls hangup <call-control-id>
84
+ telnyx calls speak <call-control-id> --text "Hello, this is an automated message"
85
+ telnyx calls speak <call-control-id> --text "Bonjour" --language fr-FR --voice female
86
+ ```
87
+
88
+ ### Verify (2FA)
89
+
90
+ ```bash
91
+ telnyx verify send --to +1XXX
92
+ telnyx verify send --to +1XXX --type sms
93
+ telnyx verify send --to +1XXX --type call
94
+ telnyx verify check <verification-id> --code 123456
95
+ telnyx verify list
96
+ ```
97
+
98
+ ### Connections
99
+
100
+ ```bash
101
+ telnyx connections list
102
+ telnyx connections get <connection-id>
103
+ ```
104
+
105
+ ### Messaging Profiles
106
+
107
+ ```bash
108
+ telnyx profiles list
109
+ telnyx profiles get <profile-id>
110
+ ```
111
+
112
+ ## JSON Output
113
+
114
+ All commands support `--json` for structured output:
115
+
116
+ ```bash
117
+ telnyx numbers list --json | jq '.[].phone_number'
118
+ telnyx messages list --json | jq '.[0]'
119
+ ```
120
+
121
+ ## Why CLI > MCP?
122
+
123
+ No server to run. No protocol overhead. Just install and go.
124
+
125
+ - **Simpler** — Just a binary you call directly
126
+ - **Composable** — Pipe to `jq`, `grep`, `awk`
127
+ - **Scriptable** — Works in cron jobs, CI/CD, shell scripts
128
+
129
+ ## License
130
+
131
+ MIT — Part of the [Kill The MCP](https://killthemcp.com) project.
package/banner.svg ADDED
@@ -0,0 +1,17 @@
1
+ <svg width="800" height="200" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#0a0a0a;stop-opacity:1" />
5
+ <stop offset="100%" style="stop-color:#1a1a1a;stop-opacity:1" />
6
+ </linearGradient>
7
+ <linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
8
+ <stop offset="0%" style="stop-color:#ff6b00;stop-opacity:1" />
9
+ <stop offset="100%" style="stop-color:#ff9a00;stop-opacity:1" />
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="800" height="200" fill="url(#bgGrad)" rx="12" />
13
+ <text x="40" y="90" font-family="monospace" font-size="52" font-weight="bold" fill="url(#textGrad)">telnyx</text>
14
+ <text x="40" y="130" font-family="monospace" font-size="18" fill="#666">Communications Platform CLI</text>
15
+ <text x="40" y="165" font-family="monospace" font-size="14" fill="#444">npm install -g @ktmcp-cli/telnyx</text>
16
+ <text x="720" y="180" font-family="monospace" font-size="12" fill="#333">killthemcp.com</text>
17
+ </svg>
package/bin/telnyx.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ import(join(__dirname, '..', 'src', 'index.js'));
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@ktmcp-cli/telnyx",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Telnyx API - SMS, calls, and phone numbers",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "telnyx": "bin/telnyx.js"
9
+ },
10
+ "keywords": [
11
+ "telnyx",
12
+ "sms",
13
+ "telephony",
14
+ "voice",
15
+ "phone",
16
+ "cli",
17
+ "api",
18
+ "ktmcp"
19
+ ],
20
+ "author": "KTMCP",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "commander": "^12.0.0",
24
+ "axios": "^1.6.7",
25
+ "chalk": "^5.3.0",
26
+ "ora": "^8.0.1",
27
+ "conf": "^12.0.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/ktmcp-cli/telnyx.git"
35
+ },
36
+ "homepage": "https://killthemcp.com/telnyx-cli",
37
+ "bugs": {
38
+ "url": "https://github.com/ktmcp-cli/telnyx/issues"
39
+ }
40
+ }
package/src/api.js ADDED
@@ -0,0 +1,157 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const BASE_URL = 'https://api.telnyx.com/v2';
5
+
6
+ function getClient() {
7
+ const apiKey = getConfig('apiKey');
8
+ if (!apiKey) {
9
+ throw new Error('API key not configured. Run: telnyx config set --api-key YOUR_KEY');
10
+ }
11
+
12
+ return axios.create({
13
+ baseURL: BASE_URL,
14
+ headers: {
15
+ 'Authorization': `Bearer ${apiKey}`,
16
+ 'Content-Type': 'application/json',
17
+ 'Accept': 'application/json'
18
+ }
19
+ });
20
+ }
21
+
22
+ async function request(method, path, data = null, params = null) {
23
+ const client = getClient();
24
+ try {
25
+ const response = await client.request({ method, url: path, data, params });
26
+ return response.data;
27
+ } catch (error) {
28
+ const errData = error.response?.data;
29
+ const msg = errData?.errors?.[0]?.detail ||
30
+ errData?.errors?.[0]?.title ||
31
+ errData?.error?.message ||
32
+ error.message;
33
+ const code = error.response?.status;
34
+ throw new Error(`API Error ${code}: ${msg}`);
35
+ }
36
+ }
37
+
38
+ // ============================================================
39
+ // Messaging
40
+ // ============================================================
41
+
42
+ export async function sendMessage({ from, to, text, subject, mediaUrls }) {
43
+ const body = { from, to, text };
44
+ if (subject) body.subject = subject;
45
+ if (mediaUrls) body.media_urls = mediaUrls;
46
+ return await request('POST', '/messages', body);
47
+ }
48
+
49
+ export async function listMessages(params = {}) {
50
+ const data = await request('GET', '/messages', null, params);
51
+ return data.data || [];
52
+ }
53
+
54
+ export async function getMessage(id) {
55
+ const data = await request('GET', `/messages/${id}`);
56
+ return data.data || null;
57
+ }
58
+
59
+ // ============================================================
60
+ // Phone Numbers
61
+ // ============================================================
62
+
63
+ export async function listPhoneNumbers(params = {}) {
64
+ const data = await request('GET', '/phone_numbers', null, params);
65
+ return data.data || [];
66
+ }
67
+
68
+ export async function getPhoneNumber(id) {
69
+ const data = await request('GET', `/phone_numbers/${id}`);
70
+ return data.data || null;
71
+ }
72
+
73
+ export async function searchAvailableNumbers(params = {}) {
74
+ const data = await request('GET', '/available_phone_numbers', null, params);
75
+ return data.data || [];
76
+ }
77
+
78
+ export async function orderPhoneNumber({ phoneNumber, connectionId }) {
79
+ const body = { phone_numbers: [{ phone_number: phoneNumber }] };
80
+ if (connectionId) body.connection_id = connectionId;
81
+ return await request('POST', '/number_orders', body);
82
+ }
83
+
84
+ // ============================================================
85
+ // Calls
86
+ // ============================================================
87
+
88
+ export async function createCall({ to, from, connectionId, webhookUrl }) {
89
+ const body = { to, from };
90
+ if (connectionId) body.connection_id = connectionId;
91
+ if (webhookUrl) body.webhook_url = webhookUrl;
92
+ return await request('POST', '/calls', body);
93
+ }
94
+
95
+ export async function getCall(callControlId) {
96
+ const data = await request('GET', `/calls/${callControlId}`);
97
+ return data.data || null;
98
+ }
99
+
100
+ export async function hangupCall(callControlId) {
101
+ return await request('POST', `/calls/${callControlId}/actions/hangup`, {});
102
+ }
103
+
104
+ export async function speakText(callControlId, { payload, voice, language }) {
105
+ return await request('POST', `/calls/${callControlId}/actions/speak`, {
106
+ payload,
107
+ voice: voice || 'female',
108
+ language: language || 'en-US'
109
+ });
110
+ }
111
+
112
+ // ============================================================
113
+ // Connections (SIP Trunks)
114
+ // ============================================================
115
+
116
+ export async function listConnections(params = {}) {
117
+ const data = await request('GET', '/connections', null, params);
118
+ return data.data || [];
119
+ }
120
+
121
+ export async function getConnection(id) {
122
+ const data = await request('GET', `/connections/${id}`);
123
+ return data.data || null;
124
+ }
125
+
126
+ // ============================================================
127
+ // Verify (2FA)
128
+ // ============================================================
129
+
130
+ export async function sendVerification({ phoneNumber, type, verifyProfileId }) {
131
+ const body = { phone_number: phoneNumber, type: type || 'sms' };
132
+ if (verifyProfileId) body.verify_profile_id = verifyProfileId;
133
+ return await request('POST', '/verifications', body);
134
+ }
135
+
136
+ export async function verifyCode({ verificationId, code }) {
137
+ return await request('POST', `/verifications/${verificationId}/actions/verify`, { code });
138
+ }
139
+
140
+ export async function listVerifications(params = {}) {
141
+ const data = await request('GET', '/verifications', null, params);
142
+ return data.data || [];
143
+ }
144
+
145
+ // ============================================================
146
+ // Messaging Profiles
147
+ // ============================================================
148
+
149
+ export async function listMessagingProfiles(params = {}) {
150
+ const data = await request('GET', '/messaging_profiles', null, params);
151
+ return data.data || [];
152
+ }
153
+
154
+ export async function getMessagingProfile(id) {
155
+ const data = await request('GET', `/messaging_profiles/${id}`);
156
+ return data.data || null;
157
+ }
package/src/config.js ADDED
@@ -0,0 +1,33 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'ktmcp-telnyx',
5
+ schema: {
6
+ apiKey: {
7
+ type: 'string',
8
+ default: ''
9
+ }
10
+ }
11
+ });
12
+
13
+ export function getConfig(key) {
14
+ return config.get(key);
15
+ }
16
+
17
+ export function setConfig(key, value) {
18
+ config.set(key, value);
19
+ }
20
+
21
+ export function getAllConfig() {
22
+ return config.store;
23
+ }
24
+
25
+ export function clearConfig() {
26
+ config.clear();
27
+ }
28
+
29
+ export function isConfigured() {
30
+ return !!config.get('apiKey');
31
+ }
32
+
33
+ export default config;
package/src/index.js ADDED
@@ -0,0 +1,683 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured } from './config.js';
5
+ import {
6
+ sendMessage,
7
+ listMessages,
8
+ getMessage,
9
+ listPhoneNumbers,
10
+ getPhoneNumber,
11
+ searchAvailableNumbers,
12
+ orderPhoneNumber,
13
+ createCall,
14
+ getCall,
15
+ hangupCall,
16
+ speakText,
17
+ listConnections,
18
+ getConnection,
19
+ sendVerification,
20
+ verifyCode,
21
+ listVerifications,
22
+ listMessagingProfiles,
23
+ getMessagingProfile
24
+ } from './api.js';
25
+
26
+ const program = new Command();
27
+
28
+ // ============================================================
29
+ // Helpers
30
+ // ============================================================
31
+
32
+ function printSuccess(message) {
33
+ console.log(chalk.green('✓') + ' ' + message);
34
+ }
35
+
36
+ function printError(message) {
37
+ console.error(chalk.red('✗') + ' ' + message);
38
+ }
39
+
40
+ function printTable(data, columns) {
41
+ if (!data || data.length === 0) {
42
+ console.log(chalk.yellow('No results found.'));
43
+ return;
44
+ }
45
+
46
+ const widths = {};
47
+ columns.forEach(col => {
48
+ widths[col.key] = col.label.length;
49
+ data.forEach(row => {
50
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
51
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
52
+ });
53
+ widths[col.key] = Math.min(widths[col.key], 40);
54
+ });
55
+
56
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
57
+ console.log(chalk.bold(chalk.cyan(header)));
58
+ console.log(chalk.dim('─'.repeat(header.length)));
59
+
60
+ data.forEach(row => {
61
+ const line = columns.map(col => {
62
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
63
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
64
+ }).join(' ');
65
+ console.log(line);
66
+ });
67
+
68
+ console.log(chalk.dim(`\n${data.length} result(s)`));
69
+ }
70
+
71
+ function printJson(data) {
72
+ console.log(JSON.stringify(data, null, 2));
73
+ }
74
+
75
+ async function withSpinner(message, fn) {
76
+ const spinner = ora(message).start();
77
+ try {
78
+ const result = await fn();
79
+ spinner.stop();
80
+ return result;
81
+ } catch (error) {
82
+ spinner.stop();
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ function requireAuth() {
88
+ if (!isConfigured()) {
89
+ printError('Telnyx API key not configured.');
90
+ console.log('\nRun the following to configure:');
91
+ console.log(chalk.cyan(' telnyx config set --api-key YOUR_API_KEY'));
92
+ console.log('\nGet your API key from: https://portal.telnyx.com/#/app/api-keys');
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ // ============================================================
98
+ // Program metadata
99
+ // ============================================================
100
+
101
+ program
102
+ .name('telnyx')
103
+ .description(chalk.bold('Telnyx CLI') + ' - Communications platform from your terminal')
104
+ .version('1.0.0');
105
+
106
+ // ============================================================
107
+ // CONFIG
108
+ // ============================================================
109
+
110
+ const configCmd = program.command('config').description('Manage CLI configuration');
111
+
112
+ configCmd
113
+ .command('set')
114
+ .description('Set configuration values')
115
+ .option('--api-key <key>', 'Telnyx API key')
116
+ .action((options) => {
117
+ if (options.apiKey) {
118
+ setConfig('apiKey', options.apiKey);
119
+ printSuccess('API key set');
120
+ } else {
121
+ printError('No options provided. Use --api-key');
122
+ }
123
+ });
124
+
125
+ configCmd
126
+ .command('show')
127
+ .description('Show current configuration')
128
+ .action(() => {
129
+ const apiKey = getConfig('apiKey');
130
+ console.log(chalk.bold('\nTelnyx CLI Configuration\n'));
131
+ console.log('API Key: ', apiKey ? chalk.green(apiKey.substring(0, 8) + '...') : chalk.red('not set'));
132
+ console.log('');
133
+ });
134
+
135
+ // ============================================================
136
+ // MESSAGES
137
+ // ============================================================
138
+
139
+ const messagesCmd = program.command('messages').description('Send and manage SMS/MMS messages');
140
+
141
+ messagesCmd
142
+ .command('send')
143
+ .description('Send an SMS or MMS message')
144
+ .requiredOption('--from <number>', 'From phone number (E.164 format, e.g. +12025551234)')
145
+ .requiredOption('--to <number>', 'To phone number (E.164 format)')
146
+ .requiredOption('--text <message>', 'Message text')
147
+ .option('--media-urls <urls>', 'Comma-separated media URLs (for MMS)')
148
+ .option('--json', 'Output as JSON')
149
+ .action(async (options) => {
150
+ requireAuth();
151
+ try {
152
+ const params = { from: options.from, to: options.to, text: options.text };
153
+ if (options.mediaUrls) params.mediaUrls = options.mediaUrls.split(',');
154
+
155
+ const result = await withSpinner('Sending message...', () => sendMessage(params));
156
+
157
+ if (options.json) {
158
+ printJson(result);
159
+ return;
160
+ }
161
+
162
+ printSuccess(`Message sent!`);
163
+ console.log('Message ID: ', chalk.cyan(result.data?.id || 'N/A'));
164
+ console.log('From: ', result.data?.from?.phone_number || options.from);
165
+ console.log('To: ', result.data?.to?.[0]?.phone_number || options.to);
166
+ console.log('Status: ', result.data?.to?.[0]?.status || 'queued');
167
+ console.log('');
168
+ } catch (error) {
169
+ printError(error.message);
170
+ process.exit(1);
171
+ }
172
+ });
173
+
174
+ messagesCmd
175
+ .command('list')
176
+ .description('List recent messages')
177
+ .option('--page-size <n>', 'Results per page', '20')
178
+ .option('--json', 'Output as JSON')
179
+ .action(async (options) => {
180
+ requireAuth();
181
+ try {
182
+ const messages = await withSpinner('Fetching messages...', () =>
183
+ listMessages({ page_size: options.pageSize })
184
+ );
185
+
186
+ if (options.json) {
187
+ printJson(messages);
188
+ return;
189
+ }
190
+
191
+ printTable(messages, [
192
+ { key: 'id', label: 'ID', format: (v) => (v || '').substring(0, 12) + '...' },
193
+ { key: 'from', label: 'From', format: (v) => typeof v === 'object' ? v.phone_number : v },
194
+ { key: 'to', label: 'To', format: (v) => Array.isArray(v) ? v[0]?.phone_number : v },
195
+ { key: 'text', label: 'Text', format: (v) => (v || '').substring(0, 30) },
196
+ { key: 'direction', label: 'Direction' },
197
+ { key: 'created_at', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
198
+ ]);
199
+ } catch (error) {
200
+ printError(error.message);
201
+ process.exit(1);
202
+ }
203
+ });
204
+
205
+ messagesCmd
206
+ .command('get <message-id>')
207
+ .description('Get a specific message')
208
+ .option('--json', 'Output as JSON')
209
+ .action(async (messageId, options) => {
210
+ requireAuth();
211
+ try {
212
+ const message = await withSpinner('Fetching message...', () => getMessage(messageId));
213
+
214
+ if (!message) {
215
+ printError('Message not found');
216
+ process.exit(1);
217
+ }
218
+
219
+ if (options.json) {
220
+ printJson(message);
221
+ return;
222
+ }
223
+
224
+ console.log(chalk.bold('\nMessage Details\n'));
225
+ console.log('ID: ', chalk.cyan(message.id));
226
+ console.log('From: ', typeof message.from === 'object' ? message.from?.phone_number : message.from);
227
+ console.log('To: ', Array.isArray(message.to) ? message.to[0]?.phone_number : message.to);
228
+ console.log('Text: ', message.text || 'N/A');
229
+ console.log('Direction: ', message.direction);
230
+ console.log('Status: ', message.to?.[0]?.status || 'N/A');
231
+ console.log('Created: ', message.created_at ? new Date(message.created_at).toLocaleString() : 'N/A');
232
+ console.log('');
233
+ } catch (error) {
234
+ printError(error.message);
235
+ process.exit(1);
236
+ }
237
+ });
238
+
239
+ // ============================================================
240
+ // PHONE NUMBERS
241
+ // ============================================================
242
+
243
+ const numbersCmd = program.command('numbers').description('Manage phone numbers');
244
+
245
+ numbersCmd
246
+ .command('list')
247
+ .description('List your phone numbers')
248
+ .option('--page-size <n>', 'Results per page', '20')
249
+ .option('--json', 'Output as JSON')
250
+ .action(async (options) => {
251
+ requireAuth();
252
+ try {
253
+ const numbers = await withSpinner('Fetching phone numbers...', () =>
254
+ listPhoneNumbers({ page_size: options.pageSize })
255
+ );
256
+
257
+ if (options.json) {
258
+ printJson(numbers);
259
+ return;
260
+ }
261
+
262
+ printTable(numbers, [
263
+ { key: 'id', label: 'ID', format: (v) => (v || '').substring(0, 12) + '...' },
264
+ { key: 'phone_number', label: 'Phone Number' },
265
+ { key: 'status', label: 'Status' },
266
+ { key: 'country_code', label: 'Country' },
267
+ { key: 'type', label: 'Type' },
268
+ { key: 'created_at', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
269
+ ]);
270
+ } catch (error) {
271
+ printError(error.message);
272
+ process.exit(1);
273
+ }
274
+ });
275
+
276
+ numbersCmd
277
+ .command('get <number-id>')
278
+ .description('Get details for a specific phone number')
279
+ .option('--json', 'Output as JSON')
280
+ .action(async (numberId, options) => {
281
+ requireAuth();
282
+ try {
283
+ const number = await withSpinner('Fetching phone number...', () => getPhoneNumber(numberId));
284
+
285
+ if (!number) {
286
+ printError('Phone number not found');
287
+ process.exit(1);
288
+ }
289
+
290
+ if (options.json) {
291
+ printJson(number);
292
+ return;
293
+ }
294
+
295
+ console.log(chalk.bold('\nPhone Number Details\n'));
296
+ console.log('ID: ', chalk.cyan(number.id));
297
+ console.log('Number: ', chalk.bold(number.phone_number));
298
+ console.log('Status: ', number.status);
299
+ console.log('Country: ', number.country_code || 'N/A');
300
+ console.log('Type: ', number.type || 'N/A');
301
+ console.log('Features: ', (number.features || []).join(', ') || 'N/A');
302
+ console.log('');
303
+ } catch (error) {
304
+ printError(error.message);
305
+ process.exit(1);
306
+ }
307
+ });
308
+
309
+ numbersCmd
310
+ .command('search')
311
+ .description('Search available phone numbers to buy')
312
+ .option('--country <code>', 'Country code (e.g. US)', 'US')
313
+ .option('--area-code <code>', 'Area code to search in')
314
+ .option('--contains <digits>', 'Filter numbers containing these digits')
315
+ .option('--limit <n>', 'Max results', '10')
316
+ .option('--json', 'Output as JSON')
317
+ .action(async (options) => {
318
+ requireAuth();
319
+ try {
320
+ const params = {
321
+ country_code: options.country,
322
+ limit: options.limit
323
+ };
324
+ if (options.areaCode) params.national_destination_code = options.areaCode;
325
+ if (options.contains) params.phone_number_type = 'local';
326
+
327
+ const numbers = await withSpinner('Searching available numbers...', () =>
328
+ searchAvailableNumbers(params)
329
+ );
330
+
331
+ if (options.json) {
332
+ printJson(numbers);
333
+ return;
334
+ }
335
+
336
+ printTable(numbers, [
337
+ { key: 'phone_number', label: 'Phone Number' },
338
+ { key: 'region_name', label: 'Region' },
339
+ { key: 'phone_number_type', label: 'Type' },
340
+ { key: 'cost', label: 'Monthly Cost', format: (v, row) => row.cost?.monthly?.amount ? `$${row.cost.monthly.amount} ${row.cost.monthly.currency}` : 'N/A' }
341
+ ]);
342
+ } catch (error) {
343
+ printError(error.message);
344
+ process.exit(1);
345
+ }
346
+ });
347
+
348
+ numbersCmd
349
+ .command('order <phone-number>')
350
+ .description('Order a phone number')
351
+ .option('--connection-id <id>', 'Connection/trunk ID to assign to')
352
+ .option('--json', 'Output as JSON')
353
+ .action(async (phoneNumber, options) => {
354
+ requireAuth();
355
+ try {
356
+ const result = await withSpinner(`Ordering ${phoneNumber}...`, () =>
357
+ orderPhoneNumber({ phoneNumber, connectionId: options.connectionId })
358
+ );
359
+
360
+ if (options.json) {
361
+ printJson(result);
362
+ return;
363
+ }
364
+
365
+ printSuccess(`Order placed for ${chalk.bold(phoneNumber)}`);
366
+ console.log('Order ID: ', result.data?.id || 'N/A');
367
+ console.log('Status: ', result.data?.status || 'N/A');
368
+ console.log('');
369
+ } catch (error) {
370
+ printError(error.message);
371
+ process.exit(1);
372
+ }
373
+ });
374
+
375
+ // ============================================================
376
+ // CALLS
377
+ // ============================================================
378
+
379
+ const callsCmd = program.command('calls').description('Make and manage phone calls');
380
+
381
+ callsCmd
382
+ .command('create')
383
+ .description('Initiate a new call')
384
+ .requiredOption('--to <number>', 'Destination number (E.164 format)')
385
+ .requiredOption('--from <number>', 'Caller ID (E.164 format)')
386
+ .option('--connection-id <id>', 'Telnyx connection ID')
387
+ .option('--webhook-url <url>', 'Webhook URL for call events')
388
+ .option('--json', 'Output as JSON')
389
+ .action(async (options) => {
390
+ requireAuth();
391
+ try {
392
+ const result = await withSpinner(`Calling ${options.to}...`, () =>
393
+ createCall({ to: options.to, from: options.from, connectionId: options.connectionId, webhookUrl: options.webhookUrl })
394
+ );
395
+
396
+ if (options.json) {
397
+ printJson(result);
398
+ return;
399
+ }
400
+
401
+ printSuccess('Call initiated!');
402
+ console.log('Call Control ID: ', chalk.cyan(result.data?.call_control_id || 'N/A'));
403
+ console.log('Call Session ID: ', result.data?.call_session_id || 'N/A');
404
+ console.log('State: ', result.data?.state || 'N/A');
405
+ console.log('');
406
+ } catch (error) {
407
+ printError(error.message);
408
+ process.exit(1);
409
+ }
410
+ });
411
+
412
+ callsCmd
413
+ .command('hangup <call-control-id>')
414
+ .description('Hang up an active call')
415
+ .option('--json', 'Output as JSON')
416
+ .action(async (callControlId, options) => {
417
+ requireAuth();
418
+ try {
419
+ const result = await withSpinner('Hanging up...', () => hangupCall(callControlId));
420
+ if (options.json) {
421
+ printJson(result);
422
+ return;
423
+ }
424
+ printSuccess('Call hung up');
425
+ } catch (error) {
426
+ printError(error.message);
427
+ process.exit(1);
428
+ }
429
+ });
430
+
431
+ callsCmd
432
+ .command('speak <call-control-id>')
433
+ .description('Speak text on an active call (text-to-speech)')
434
+ .requiredOption('--text <message>', 'Text to speak')
435
+ .option('--voice <voice>', 'Voice (female|male)', 'female')
436
+ .option('--language <lang>', 'Language code', 'en-US')
437
+ .option('--json', 'Output as JSON')
438
+ .action(async (callControlId, options) => {
439
+ requireAuth();
440
+ try {
441
+ const result = await withSpinner('Speaking text...', () =>
442
+ speakText(callControlId, { payload: options.text, voice: options.voice, language: options.language })
443
+ );
444
+ if (options.json) {
445
+ printJson(result);
446
+ return;
447
+ }
448
+ printSuccess('Text-to-speech started');
449
+ } catch (error) {
450
+ printError(error.message);
451
+ process.exit(1);
452
+ }
453
+ });
454
+
455
+ // ============================================================
456
+ // VERIFY
457
+ // ============================================================
458
+
459
+ const verifyCmd = program.command('verify').description('Send and check 2FA verification codes');
460
+
461
+ verifyCmd
462
+ .command('send')
463
+ .description('Send a verification code')
464
+ .requiredOption('--to <number>', 'Phone number to verify (E.164 format)')
465
+ .option('--type <type>', 'Delivery type (sms|call|flash)', 'sms')
466
+ .option('--profile-id <id>', 'Verify profile ID')
467
+ .option('--json', 'Output as JSON')
468
+ .action(async (options) => {
469
+ requireAuth();
470
+ try {
471
+ const result = await withSpinner(`Sending ${options.type.toUpperCase()} code to ${options.to}...`, () =>
472
+ sendVerification({ phoneNumber: options.to, type: options.type, verifyProfileId: options.profileId })
473
+ );
474
+
475
+ if (options.json) {
476
+ printJson(result);
477
+ return;
478
+ }
479
+
480
+ printSuccess('Verification code sent!');
481
+ console.log('Verification ID: ', chalk.cyan(result.data?.id || 'N/A'));
482
+ console.log('Phone: ', options.to);
483
+ console.log('Type: ', options.type.toUpperCase());
484
+ console.log('');
485
+ console.log(chalk.dim('Use: telnyx verify check <verification-id> --code <code>'));
486
+ } catch (error) {
487
+ printError(error.message);
488
+ process.exit(1);
489
+ }
490
+ });
491
+
492
+ verifyCmd
493
+ .command('check <verification-id>')
494
+ .description('Check a verification code')
495
+ .requiredOption('--code <code>', 'The verification code to check')
496
+ .option('--json', 'Output as JSON')
497
+ .action(async (verificationId, options) => {
498
+ requireAuth();
499
+ try {
500
+ const result = await withSpinner('Verifying code...', () =>
501
+ verifyCode({ verificationId, code: options.code })
502
+ );
503
+
504
+ if (options.json) {
505
+ printJson(result);
506
+ return;
507
+ }
508
+
509
+ const valid = result.data?.response_code === 'accepted';
510
+ if (valid) {
511
+ printSuccess('Code verified successfully!');
512
+ } else {
513
+ printError('Code verification failed: ' + (result.data?.response_code || 'invalid'));
514
+ process.exit(1);
515
+ }
516
+ } catch (error) {
517
+ printError(error.message);
518
+ process.exit(1);
519
+ }
520
+ });
521
+
522
+ verifyCmd
523
+ .command('list')
524
+ .description('List recent verifications')
525
+ .option('--json', 'Output as JSON')
526
+ .action(async (options) => {
527
+ requireAuth();
528
+ try {
529
+ const verifications = await withSpinner('Fetching verifications...', () => listVerifications());
530
+
531
+ if (options.json) {
532
+ printJson(verifications);
533
+ return;
534
+ }
535
+
536
+ printTable(verifications, [
537
+ { key: 'id', label: 'ID', format: (v) => (v || '').substring(0, 12) + '...' },
538
+ { key: 'phone_number', label: 'Phone Number' },
539
+ { key: 'type', label: 'Type' },
540
+ { key: 'status', label: 'Status' },
541
+ { key: 'created_at', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
542
+ ]);
543
+ } catch (error) {
544
+ printError(error.message);
545
+ process.exit(1);
546
+ }
547
+ });
548
+
549
+ // ============================================================
550
+ // CONNECTIONS
551
+ // ============================================================
552
+
553
+ const connectionsCmd = program.command('connections').description('Manage SIP connections and trunks');
554
+
555
+ connectionsCmd
556
+ .command('list')
557
+ .description('List all connections')
558
+ .option('--json', 'Output as JSON')
559
+ .action(async (options) => {
560
+ requireAuth();
561
+ try {
562
+ const connections = await withSpinner('Fetching connections...', () => listConnections());
563
+
564
+ if (options.json) {
565
+ printJson(connections);
566
+ return;
567
+ }
568
+
569
+ printTable(connections, [
570
+ { key: 'id', label: 'ID', format: (v) => (v || '').substring(0, 12) + '...' },
571
+ { key: 'connection_name', label: 'Name' },
572
+ { key: 'record_type', label: 'Type' },
573
+ { key: 'active', label: 'Active', format: (v) => v ? chalk.green('Yes') : 'No' },
574
+ { key: 'created_at', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
575
+ ]);
576
+ } catch (error) {
577
+ printError(error.message);
578
+ process.exit(1);
579
+ }
580
+ });
581
+
582
+ connectionsCmd
583
+ .command('get <connection-id>')
584
+ .description('Get a specific connection')
585
+ .option('--json', 'Output as JSON')
586
+ .action(async (connectionId, options) => {
587
+ requireAuth();
588
+ try {
589
+ const connection = await withSpinner('Fetching connection...', () => getConnection(connectionId));
590
+
591
+ if (!connection) {
592
+ printError('Connection not found');
593
+ process.exit(1);
594
+ }
595
+
596
+ if (options.json) {
597
+ printJson(connection);
598
+ return;
599
+ }
600
+
601
+ console.log(chalk.bold('\nConnection Details\n'));
602
+ console.log('ID: ', chalk.cyan(connection.id));
603
+ console.log('Name: ', chalk.bold(connection.connection_name || 'N/A'));
604
+ console.log('Type: ', connection.record_type || 'N/A');
605
+ console.log('Active: ', connection.active ? chalk.green('Yes') : 'No');
606
+ console.log('');
607
+ } catch (error) {
608
+ printError(error.message);
609
+ process.exit(1);
610
+ }
611
+ });
612
+
613
+ // ============================================================
614
+ // MESSAGING PROFILES
615
+ // ============================================================
616
+
617
+ const profilesCmd = program.command('profiles').description('Manage messaging profiles');
618
+
619
+ profilesCmd
620
+ .command('list')
621
+ .description('List messaging profiles')
622
+ .option('--json', 'Output as JSON')
623
+ .action(async (options) => {
624
+ requireAuth();
625
+ try {
626
+ const profiles = await withSpinner('Fetching messaging profiles...', () => listMessagingProfiles());
627
+
628
+ if (options.json) {
629
+ printJson(profiles);
630
+ return;
631
+ }
632
+
633
+ printTable(profiles, [
634
+ { key: 'id', label: 'ID', format: (v) => (v || '').substring(0, 12) + '...' },
635
+ { key: 'name', label: 'Name' },
636
+ { key: 'enabled', label: 'Enabled', format: (v) => v ? chalk.green('Yes') : 'No' },
637
+ { key: 'created_at', label: 'Created', format: (v) => v ? new Date(v).toLocaleDateString() : '' }
638
+ ]);
639
+ } catch (error) {
640
+ printError(error.message);
641
+ process.exit(1);
642
+ }
643
+ });
644
+
645
+ profilesCmd
646
+ .command('get <profile-id>')
647
+ .description('Get a specific messaging profile')
648
+ .option('--json', 'Output as JSON')
649
+ .action(async (profileId, options) => {
650
+ requireAuth();
651
+ try {
652
+ const profile = await withSpinner('Fetching messaging profile...', () => getMessagingProfile(profileId));
653
+
654
+ if (!profile) {
655
+ printError('Messaging profile not found');
656
+ process.exit(1);
657
+ }
658
+
659
+ if (options.json) {
660
+ printJson(profile);
661
+ return;
662
+ }
663
+
664
+ console.log(chalk.bold('\nMessaging Profile Details\n'));
665
+ console.log('ID: ', chalk.cyan(profile.id));
666
+ console.log('Name: ', chalk.bold(profile.name || 'N/A'));
667
+ console.log('Enabled: ', profile.enabled ? chalk.green('Yes') : 'No');
668
+ console.log('');
669
+ } catch (error) {
670
+ printError(error.message);
671
+ process.exit(1);
672
+ }
673
+ });
674
+
675
+ // ============================================================
676
+ // Parse
677
+ // ============================================================
678
+
679
+ program.parse(process.argv);
680
+
681
+ if (process.argv.length <= 2) {
682
+ program.help();
683
+ }