cx-vcc
Version:
Cloudonix Agentic Voice Connector Tool
528 lines (463 loc) • 30.7 kB
JavaScript
const chalk = require('chalk');
const { getConfig, getDomainConfig } = require('../utils/config');
const VapiApiService = require('../services/vapiApi');
const RetellApiService = require('../services/retellApi');
const ElevenLabsAgentProvider = require('../services/11LabsAgentProvider');
async function displayCommand(options) {
const { domain, remote } = options;
const config = getConfig();
if (domain) {
const domainConfig = getDomainConfig(domain);
if (domainConfig) {
console.log(chalk.blue.bold(`Configuration for domain: ${domain}`));
displayDomainConfig(domain, domainConfig);
} else {
console.error(chalk.red(`Domain ${domain} not found in configuration.`));
}
} else {
console.log(chalk.blue.bold('All domain configurations:'));
if (Object.keys(config.domains).length === 0) {
console.log(chalk.yellow('No domains configured.'));
} else {
Object.entries(config.domains).forEach(([domainName, domainConfig]) => {
displayDomainConfig(domainName, domainConfig);
});
}
await displayProviderConfig('VAPI', config.vapi, remote);
await displayProviderConfig('Retell', config.retell, remote);
await displayProviderConfig('11Labs', config.elevenlabs, remote);
}
}
function displayDomainConfig(domainName, domainConfig) {
console.log(chalk.green.bold(`\nDomain: ${domainName}`));
console.log(chalk.cyan('API Key:'), domainConfig.apiKey ? '********' : 'Not set');
console.log(chalk.cyan('Alias:'), domainConfig.alias || 'Not set');
console.log(chalk.cyan('Auto Alias:'), domainConfig.autoAlias || 'Not set');
console.log(chalk.cyan('Inbound SIP URI:'), domainConfig.inboundSipUri || 'Not set');
console.log(chalk.cyan('Tenant:'), domainConfig.tenant || 'Not set');
const providers = ['VAPI', 'Retell', '11Labs'];
const phoneNumbers = {};
providers.forEach(providerName => {
if (domainConfig[providerName.toLowerCase()]) {
console.log(chalk.cyan(`${providerName} Trunk Credential ID:`),
domainConfig[providerName.toLowerCase()].trunkCredentialId || 'Not set');
phoneNumbers[providerName] = domainConfig[providerName.toLowerCase()].phoneNumbers;
}
});
displayPhoneNumbers(phoneNumbers);
}
function displayProviderInDomain(providerName, providerConfig) {
if (providerConfig) {
console.log(chalk.cyan(`${providerName} Trunk Credential ID:`), providerConfig.trunkCredentialId || 'Not set');
displayPhoneNumbers(providerName, providerConfig.phoneNumbers);
}
}
async function displayProviderConfig(providerName, providerConfig, remote) {
console.log(chalk.blue.bold(`\n${providerName} Configuration:`));
if (providerConfig) {
console.log(chalk.cyan('API Key:'), providerConfig.apiKey ? '********' : 'Not set');
// Check for apiUrl differently depending on provider
let apiUrl = 'Not set';
if (providerConfig.apiUrl) {
apiUrl = providerConfig.apiUrl;
} else if (providerName === '11Labs' && providerConfig.apiUrl) {
// For 11Labs, we use a different property name
apiUrl = providerConfig.apiUrl;
}
// If still not set, use default API URLs for each provider
if (apiUrl === 'Not set') {
if (providerName === 'VAPI') {
apiUrl = 'https://api.vapi.ai (default)';
} else if (providerName === 'Retell') {
apiUrl = 'https://api.retellai.com (default)';
} else if (providerName === '11Labs') {
apiUrl = 'https://api.elevenlabs.io (default)';
}
}
console.log(chalk.cyan('API URL:'), apiUrl);
// Debug info to see the actual config structure
if (process.argv.includes('--debug')) {
console.log('\n=== Provider Config Debug ===');
console.log('Provider:', providerName);
console.log('Config Keys:', Object.keys(providerConfig));
console.log('========================\n');
}
// Display local phone numbers
displayPhoneNumbers({ [providerName]: providerConfig.phoneNumbers });
// Display remote phone numbers if remote flag is set
if (remote && providerConfig.apiKey) {
try {
console.log(chalk.magenta.bold(`\nRemote ${providerName} Phone Numbers:`));
// Create and start a loading animation
const loadingChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;
const loadingInterval = setInterval(() => {
process.stdout.write(`\r${chalk.cyan(loadingChars[i])} Fetching ${providerName} phone numbers...`);
i = (i + 1) % loadingChars.length;
}, 100);
let remoteNumbers = [];
if (providerName === 'VAPI') {
const vapiService = new VapiApiService(providerConfig.apiKey, providerConfig.apiUrl);
const phoneNumbersData = await vapiService.getPhoneNumbers();
if (phoneNumbersData && Array.isArray(phoneNumbersData)) {
// Get detailed information for each phone number
remoteNumbers = [];
for (const num of phoneNumbersData) {
// Only process byo-phone-number types
if (num.provider !== 'byo-phone-number') {
continue;
}
try {
const details = await vapiService.getPhoneNumberDetails(num.id);
// Fetch credential details if available
let credentialDetails = null;
if (details.credentialId) {
try {
// Update loading message for credential details
process.stdout.write(`\r${chalk.cyan(loadingChars[i % loadingChars.length])} Fetching credential details for ${details.number}...`);
credentialDetails = await vapiService.getCredentialDetails(details.credentialId);
} catch (credError) {
console.error(chalk.yellow(` Unable to fetch credential details for ID ${details.credentialId}: ${credError.message}`));
}
}
remoteNumbers.push({
...details,
id: num.id,
credentialDetails
});
} catch (error) {
console.error(chalk.red(` Error fetching details for number ${num.number}: ${error.message}`));
// Add basic info from the list if details fail
remoteNumbers.push({
number: num.number,
name: num.name,
provider: num.provider,
id: num.id,
error: 'Failed to fetch details'
});
}
}
}
} else if (providerName === 'Retell') {
const retellService = new RetellApiService(providerConfig.apiKey, providerConfig.apiUrl);
const phoneNumbersData = await retellService.getPhoneNumbers();
// Log detailed response for debugging
if (process.argv.includes('--debug')) {
console.log('\n=== Retell Phone Numbers Response ===');
console.log(JSON.stringify(phoneNumbersData, null, 2));
console.log('===================================\n');
}
if (phoneNumbersData && Array.isArray(phoneNumbersData)) {
remoteNumbers = phoneNumbersData.map(num => ({
id: num.phone_number || num.phoneNumber || 'unknown',
number: num.phone_number || num.phoneNumber || 'unknown',
phoneType: num.phone_number_type || num.phoneNumberType || 'N/A',
areaCode: num.area_code || num.areaCode,
inboundAgentId: num.inbound_agent_id || num.inboundAgentId,
outboundAgentId: num.outbound_agent_id || num.outboundAgentId,
lastModified: num.last_modification_timestamp || num.lastModificationTimestamp ?
new Date(parseInt(num.last_modification_timestamp || num.lastModificationTimestamp) * 1000).toLocaleString() : null
}));
}
} else if (providerName === '11Labs') {
try {
const elevenLabsProvider = new ElevenLabsAgentProvider(providerConfig.apiKey, providerConfig.apiUrl);
const phoneNumbersData = await elevenLabsProvider.getPhoneNumbers();
// Log detailed response for debugging
if (process.argv.includes('--debug')) {
console.log('\n=== 11Labs Phone Numbers Response ===');
console.log(JSON.stringify(phoneNumbersData, null, 2));
console.log('Phone Numbers Count:', Array.isArray(phoneNumbersData) ? phoneNumbersData.length : 'Not an array');
console.log('===================================\n');
}
if (phoneNumbersData && Array.isArray(phoneNumbersData)) {
remoteNumbers = phoneNumbersData.map(num => ({
id: num.phone_number_id || num.id || 'unknown',
number: num.phone_number || num.phoneNumber || num.number || 'unknown',
label: num.label || num.name || 'N/A',
termination_uri: num.termination_uri || num.terminationUri || 'N/A',
status: num.status || 'N/A',
created_at: num.created_at || num.createdAt || 'N/A',
source: num.source || 'api'
}));
} else {
// If we didn't get an array back, check if we have local config
if (process.argv.includes('--debug')) {
console.log('\n=== 11Labs Fallback to Local Config ===');
console.log('Provider Config:', providerConfig);
if (providerConfig.phoneNumbers) {
console.log('Local Phone Numbers:', Object.keys(providerConfig.phoneNumbers));
}
console.log('===================================\n');
}
// Try to use local config as fallback
if (providerConfig.phoneNumbers && Object.keys(providerConfig.phoneNumbers).length > 0) {
remoteNumbers = Object.entries(providerConfig.phoneNumbers).map(([number, details]) => ({
id: details.id || 'config-' + Date.now(),
number: number,
label: `[Local Config] ${number}`,
termination_uri: details.sipUri || 'N/A',
status: 'active (local)',
created_at: 'N/A',
source: 'local_fallback'
}));
}
}
} catch (error) {
console.error(`Error fetching 11Labs phone numbers: ${error.message}`);
// Try to use local config as ultimate fallback
if (providerConfig.phoneNumbers && Object.keys(providerConfig.phoneNumbers).length > 0) {
remoteNumbers = Object.entries(providerConfig.phoneNumbers).map(([number, details]) => ({
id: details.id || 'config-' + Date.now(),
number: number,
label: `[Local Config] ${number}`,
termination_uri: details.sipUri || 'N/A',
status: 'active (local)',
created_at: 'N/A',
source: 'error_fallback'
}));
}
}
}
// Make sure to clear the loading animation
try {
clearInterval(loadingInterval);
process.stdout.write('\r' + ' '.repeat(50) + '\r');
} catch (err) {
// In case the interval was already cleared
}
if (remoteNumbers.length > 0) {
remoteNumbers.forEach(num => {
let sipUri = '';
if (providerName === 'VAPI') {
sipUri = `sip:${num.number}@sip.vapi.ai`;
} else if (providerName === 'Retell') {
sipUri = `sip:${num.number}@5t4n6j0wnrl.sip.livekit.cloud:5060;transport=tcp`;
} else if (providerName === '11Labs') {
// Get the termination_uri from the result if available, otherwise construct a default one
if (num.termination_uri) {
sipUri = num.termination_uri;
} else {
const formattedNumber = num.number.startsWith('+') ? num.number.substring(1) : num.number;
sipUri = `sip:${formattedNumber}@sip.rtc.elevenlabs.io:5060;transport=tcp`;
}
}
console.log(chalk.green(` - Number: ${num.number} (${sipUri})`));
// Only show Name for VAPI and 11Labs, not for Retell
if (providerName === 'VAPI') {
console.log(chalk.yellow(` Name: ${num.name}`));
} else if (providerName === '11Labs') {
console.log(chalk.yellow(` Label: ${num.label || 'N/A'}`));
}
console.log(chalk.yellow(` ID: ${num.id}`));
if (providerName === 'VAPI') {
console.log(chalk.yellow(` Provider: ${num.provider || 'N/A'}`));
if (num.error) {
console.log(chalk.red(` Error: ${num.error}`));
} else {
// Display basic details
console.log(chalk.yellow(` Status: ${num.status || 'N/A'}`));
if (num.credentialId) {
console.log(chalk.yellow(` Credential ID: ${num.credentialId}`));
// Display credential details and gateways if available
if (num.credentialDetails) {
console.log(chalk.cyan(` Credential Details:`));
console.log(chalk.yellow(` Name: ${num.credentialDetails.name || 'N/A'}`));
console.log(chalk.yellow(` Provider: ${num.credentialDetails.provider || 'N/A'}`));
if (num.credentialDetails.gateways && num.credentialDetails.gateways.length > 0) {
console.log(chalk.cyan(` Gateways:`));
num.credentialDetails.gateways.forEach((gateway, idx) => {
console.log(chalk.yellow(` Gateway ${idx + 1}:`));
if (gateway.ip) {
console.log(chalk.yellow(` IP: ${gateway.ip}`));
}
if (gateway.port) {
console.log(chalk.yellow(` Port: ${gateway.port}`));
}
if (gateway.protocol) {
console.log(chalk.yellow(` Protocol: ${gateway.protocol}`));
}
});
}
}
}
if (num.createdAt) {
console.log(chalk.yellow(` Created: ${new Date(num.createdAt).toLocaleString()}`));
}
if (num.updatedAt) {
console.log(chalk.yellow(` Updated: ${new Date(num.updatedAt).toLocaleString()}`));
}
if (num.assistantId) {
console.log(chalk.yellow(` Assistant ID: ${num.assistantId}`));
}
if (num.orgId) {
console.log(chalk.yellow(` Organization ID: ${num.orgId}`));
}
if (num.squadId) {
console.log(chalk.yellow(` Squad ID: ${num.squadId}`));
}
// Display fallback destination if present
if (num.fallbackDestination) {
console.log(chalk.cyan(` Fallback Destination:`));
console.log(chalk.yellow(` Type: ${num.fallbackDestination.type || 'N/A'}`));
if (num.fallbackDestination.number) {
console.log(chalk.yellow(` Number: ${num.fallbackDestination.number}`));
}
if (num.fallbackDestination.callerId) {
console.log(chalk.yellow(` Caller ID: ${num.fallbackDestination.callerId}`));
}
if (num.fallbackDestination.description) {
console.log(chalk.yellow(` Description: ${num.fallbackDestination.description}`));
}
if (num.fallbackDestination.message) {
console.log(chalk.yellow(` Message: ${num.fallbackDestination.message}`));
}
}
// Display hooks if present
if (num.hooks && num.hooks.length > 0) {
console.log(chalk.cyan(` Hooks (${num.hooks.length}):`));
num.hooks.forEach((hook, index) => {
console.log(chalk.yellow(` Hook ${index + 1}:`));
console.log(chalk.yellow(` Event: ${hook.on || 'N/A'}`));
if (hook.do && hook.do.length > 0) {
console.log(chalk.yellow(` Actions: ${hook.do.map(a => a.type).join(', ')}`));
}
});
}
// Display server configuration if present
if (num.server && num.server.url) {
console.log(chalk.cyan(` Server Configuration:`));
console.log(chalk.yellow(` URL: ${num.server.url}`));
if (num.server.timeoutSeconds) {
console.log(chalk.yellow(` Timeout: ${num.server.timeoutSeconds} seconds`));
}
if (num.server.headers) {
console.log(chalk.yellow(` Custom Headers: ${Object.keys(num.server.headers).length}`));
}
}
}
} else if (providerName === 'Retell') {
// Show important properties first with better formatting
if (num.phoneType && num.phoneType !== 'N/A') {
console.log(chalk.yellow(` Phone Type: ${num.phoneType}`));
}
if (num.lastModified) {
console.log(chalk.yellow(` Last Modified: ${num.lastModified}`));
}
if (num.inboundAgentId) {
console.log(chalk.yellow(` Inbound Agent ID: ${num.inboundAgentId}`));
}
if (num.outboundAgentId) {
console.log(chalk.yellow(` Outbound Agent ID: ${num.outboundAgentId}`));
}
if (num.areaCode) {
console.log(chalk.yellow(` Area Code: ${num.areaCode}`));
}
// Display any remaining properties in the Retell response
Object.entries(num).forEach(([key, value]) => {
// Skip properties we already displayed or null/undefined values
if (key !== 'id' &&
key !== 'number' &&
key !== 'phoneType' &&
key !== 'lastModified' &&
key !== 'inboundAgentId' &&
key !== 'outboundAgentId' &&
key !== 'areaCode' &&
value != null) {
// Display properties
if (typeof value === 'object') {
console.log(chalk.yellow(` ${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: ${JSON.stringify(value)}`));
} else {
console.log(chalk.yellow(` ${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: ${value}`));
}
}
});
} else if (providerName === '11Labs') {
// Display 11Labs specific properties
if (num.error) {
console.log(chalk.red(` Error: ${num.error}`));
} else {
// Display status if available
if (num.status && num.status !== 'N/A') {
console.log(chalk.yellow(` Status: ${num.status}`));
}
// Display created_at if available
if (num.created_at && num.created_at !== 'N/A') {
// Try to format the date if it's a timestamp
let formattedDate = num.created_at;
try {
if (typeof num.created_at === 'number' || !isNaN(new Date(num.created_at).getTime())) {
formattedDate = new Date(num.created_at).toLocaleString();
}
} catch (e) {
// Keep original format if conversion fails
}
console.log(chalk.yellow(` Created: ${formattedDate}`));
}
// Display termination_uri if available (but not if it's the same as sipUri)
if (num.termination_uri && num.termination_uri !== 'N/A' && num.termination_uri !== sipUri) {
console.log(chalk.yellow(` Termination URI: ${num.termination_uri}`));
}
// Display additional properties dynamically
Object.entries(num).forEach(([key, value]) => {
if (key !== 'id' &&
key !== 'number' &&
key !== 'label' &&
key !== 'termination_uri' &&
key !== 'status' &&
key !== 'created_at' &&
value != null) {
if (typeof value === 'object') {
console.log(chalk.yellow(` ${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: ${JSON.stringify(value)}`));
} else {
console.log(chalk.yellow(` ${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}: ${value}`));
}
}
});
}
}
console.log();
});
} else {
console.log(chalk.yellow(' No remote phone numbers found.'));
}
} catch (error) {
// Clear the loading animation in case of error
try {
clearInterval(loadingInterval);
process.stdout.write('\r' + ' '.repeat(50) + '\r');
} catch (err) {
// In case the interval was already cleared
}
console.error(chalk.red(` Error fetching remote ${providerName} phone numbers: ${error.message}`));
}
}
} else {
console.log(chalk.yellow(`No ${providerName} configuration found.`));
}
}
function displayPhoneNumbers(providerPhoneNumbers) {
const hasPhoneNumbers = Object.values(providerPhoneNumbers).some(numbers => numbers && Object.keys(numbers).length > 0);
if (hasPhoneNumbers) {
console.log(chalk.cyan('Phone Numbers:'));
Object.entries(providerPhoneNumbers).forEach(([providerName, phoneNumbers]) => {
if (phoneNumbers && Object.keys(phoneNumbers).length > 0) {
console.log(chalk.yellow(` - ${providerName}`));
Object.entries(phoneNumbers).forEach(([number, config]) => {
let sipUri = '';
if (providerName === 'VAPI') {
sipUri = `sip:${number}@sip.vapi.ai`;
} else if (providerName === 'Retell') {
sipUri = `sip:${number}@5t4n6j0wnrl.sip.livekit.cloud:5060;transport=tcp`;
} else if (providerName === '11Labs') {
const formattedNumber = number.startsWith('+') ? number.substring(1) : number;
sipUri = `sip:${formattedNumber}@sip.rtc.elevenlabs.io:5060;transport=tcp`;
}
console.log(chalk.yellow(` - Number: ${number} (${sipUri})`));
});
}
});
}
// Removed the "None configured" message
}
module.exports = displayCommand;