luxlabs 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/LICENSE +37 -0
- package/README.md +161 -0
- package/commands/ab-tests.js +437 -0
- package/commands/agents.js +226 -0
- package/commands/data.js +966 -0
- package/commands/deploy.js +166 -0
- package/commands/dev.js +569 -0
- package/commands/init.js +126 -0
- package/commands/interface/boilerplate.js +52 -0
- package/commands/interface/git-utils.js +85 -0
- package/commands/interface/index.js +7 -0
- package/commands/interface/init.js +375 -0
- package/commands/interface/path.js +74 -0
- package/commands/interface.js +125 -0
- package/commands/knowledge.js +339 -0
- package/commands/link.js +127 -0
- package/commands/list.js +97 -0
- package/commands/login.js +247 -0
- package/commands/logout.js +19 -0
- package/commands/logs.js +182 -0
- package/commands/pricing.js +328 -0
- package/commands/project.js +704 -0
- package/commands/secrets.js +129 -0
- package/commands/servers.js +411 -0
- package/commands/storage.js +177 -0
- package/commands/up.js +211 -0
- package/commands/validate-data-lux.js +502 -0
- package/commands/voice-agents.js +1055 -0
- package/commands/webview.js +393 -0
- package/commands/workflows.js +836 -0
- package/lib/config.js +403 -0
- package/lib/helpers.js +189 -0
- package/lib/node-helper.js +120 -0
- package/lux.js +268 -0
- package/package.json +56 -0
- package/templates/next-env.d.ts +6 -0
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice Agents CLI Commands
|
|
3
|
+
*
|
|
4
|
+
* Manage ElevenLabs voice agents from the command line.
|
|
5
|
+
* This allows Claude Code to create, update, and manage voice agents
|
|
6
|
+
* without requiring the user to use the Lux Studio UI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const axios = require('axios');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const {
|
|
14
|
+
getApiUrl,
|
|
15
|
+
getStudioApiUrl,
|
|
16
|
+
getAuthHeaders,
|
|
17
|
+
isAuthenticated,
|
|
18
|
+
getOrgId,
|
|
19
|
+
getProjectId,
|
|
20
|
+
LUX_STUDIO_DIR,
|
|
21
|
+
} = require('../lib/config');
|
|
22
|
+
const {
|
|
23
|
+
error,
|
|
24
|
+
success,
|
|
25
|
+
info,
|
|
26
|
+
warn,
|
|
27
|
+
formatTable,
|
|
28
|
+
requireArgs,
|
|
29
|
+
parseJson,
|
|
30
|
+
} = require('../lib/helpers');
|
|
31
|
+
|
|
32
|
+
// ElevenLabs API base URL
|
|
33
|
+
const ELEVENLABS_API_URL = 'https://api.elevenlabs.io/v1';
|
|
34
|
+
|
|
35
|
+
// Lux Studio API URL for webhooks and credentials
|
|
36
|
+
const LUX_STUDIO_API_URL = process.env.LUX_STUDIO_API_URL || 'https://v2.uselux.ai';
|
|
37
|
+
|
|
38
|
+
// Built-in voice names
|
|
39
|
+
const BUILT_IN_VOICES = [
|
|
40
|
+
'Rachel', 'Drew', 'Clyde', 'Paul', 'Domi',
|
|
41
|
+
'Dave', 'Fin', 'Sarah', 'Antoni', 'Thomas',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the voice agents cache path for the current project
|
|
46
|
+
*/
|
|
47
|
+
function getCachePath() {
|
|
48
|
+
const orgId = getOrgId();
|
|
49
|
+
const projectId = getProjectId();
|
|
50
|
+
if (!orgId || !projectId) return null;
|
|
51
|
+
|
|
52
|
+
// Handle org ID format (might have org_ prefix or not)
|
|
53
|
+
const orgDir = orgId.startsWith('org_') ? orgId : `org_${orgId}`;
|
|
54
|
+
return path.join(LUX_STUDIO_DIR, orgDir, 'projects', projectId, 'voice-agents-cache.json');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the local cache
|
|
59
|
+
*/
|
|
60
|
+
function readCache() {
|
|
61
|
+
const cachePath = getCachePath();
|
|
62
|
+
if (!cachePath || !fs.existsSync(cachePath)) {
|
|
63
|
+
return { agents: [], phoneNumbers: [], lastUpdated: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return { agents: [], phoneNumbers: [], lastUpdated: null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Write to the local cache
|
|
75
|
+
*/
|
|
76
|
+
function writeCache(data) {
|
|
77
|
+
const cachePath = getCachePath();
|
|
78
|
+
if (!cachePath) return;
|
|
79
|
+
|
|
80
|
+
const dir = path.dirname(cachePath);
|
|
81
|
+
if (!fs.existsSync(dir)) {
|
|
82
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const cache = {
|
|
86
|
+
...readCache(),
|
|
87
|
+
...data,
|
|
88
|
+
lastUpdated: new Date().toISOString(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get voice agent API key from Lux platform credentials
|
|
96
|
+
* Uses the platform credential endpoint (shared across all users)
|
|
97
|
+
*/
|
|
98
|
+
async function getVoiceAgentApiKey() {
|
|
99
|
+
try {
|
|
100
|
+
// Use platform credential endpoint (shared key, not per-project)
|
|
101
|
+
const { data } = await axios.get(
|
|
102
|
+
`${LUX_STUDIO_API_URL}/api/platform/credentials/elevenlabs`,
|
|
103
|
+
{ headers: getAuthHeaders() }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Response format: { type: 'elevenlabs', data: { apiKey } }
|
|
107
|
+
if (data?.data?.apiKey) {
|
|
108
|
+
return data.data.apiKey;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// Platform credential not available
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Make ElevenLabs API request
|
|
120
|
+
*/
|
|
121
|
+
async function elevenLabsRequest(method, endpoint, apiKey, data = null) {
|
|
122
|
+
const config = {
|
|
123
|
+
method,
|
|
124
|
+
url: `${ELEVENLABS_API_URL}${endpoint}`,
|
|
125
|
+
headers: {
|
|
126
|
+
'xi-api-key': apiKey,
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (data) {
|
|
132
|
+
config.data = data;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const response = await axios(config);
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* List all agents for the current project
|
|
141
|
+
*/
|
|
142
|
+
async function listAgents(apiKey) {
|
|
143
|
+
const projectId = getProjectId();
|
|
144
|
+
const projectTag = `project-${projectId}`;
|
|
145
|
+
|
|
146
|
+
const data = await elevenLabsRequest('GET', '/convai/agents', apiKey);
|
|
147
|
+
|
|
148
|
+
// Filter to only agents tagged with this project
|
|
149
|
+
// Note: Tags are at agent.tags, not agent.metadata.tags
|
|
150
|
+
const agents = (data.agents || [])
|
|
151
|
+
.filter(agent => {
|
|
152
|
+
const tags = agent.tags || [];
|
|
153
|
+
return tags.includes(projectTag);
|
|
154
|
+
})
|
|
155
|
+
.map(agent => ({
|
|
156
|
+
id: agent.agent_id,
|
|
157
|
+
name: agent.name,
|
|
158
|
+
voice: agent.conversation_config?.agent?.prompt?.llm?.model || '',
|
|
159
|
+
voiceId: agent.conversation_config?.tts?.voice_id || '',
|
|
160
|
+
systemPrompt: agent.conversation_config?.agent?.prompt?.prompt || '',
|
|
161
|
+
greeting: agent.conversation_config?.agent?.first_message || '',
|
|
162
|
+
maxDurationSeconds: agent.conversation_config?.conversation?.max_duration_seconds || 600,
|
|
163
|
+
temperature: agent.conversation_config?.agent?.prompt?.temperature || 0.7,
|
|
164
|
+
language: agent.conversation_config?.agent?.language || 'en',
|
|
165
|
+
tags: agent.tags || [],
|
|
166
|
+
createdAt: agent.created_at,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
return agents;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get a single agent
|
|
174
|
+
*/
|
|
175
|
+
async function getAgent(apiKey, agentId) {
|
|
176
|
+
const data = await elevenLabsRequest('GET', `/convai/agents/${agentId}`, apiKey);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: data.agent_id,
|
|
180
|
+
name: data.name,
|
|
181
|
+
voice: data.conversation_config?.tts?.voice_id || '',
|
|
182
|
+
voiceId: data.conversation_config?.tts?.voice_id || '',
|
|
183
|
+
systemPrompt: data.conversation_config?.agent?.prompt?.prompt || '',
|
|
184
|
+
greeting: data.conversation_config?.agent?.first_message || '',
|
|
185
|
+
maxDurationSeconds: data.conversation_config?.conversation?.max_duration_seconds || 600,
|
|
186
|
+
temperature: data.conversation_config?.agent?.prompt?.temperature || 0.7,
|
|
187
|
+
language: data.conversation_config?.agent?.language || 'en',
|
|
188
|
+
tags: data.tags || [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Voice name to ID mapping (same as elevenlabs-client.ts)
|
|
193
|
+
const VOICE_MAP = {
|
|
194
|
+
Rachel: '21m00Tcm4TlvDq8ikWAM',
|
|
195
|
+
Drew: '29vD33N1CtxCmqQRPOHJ',
|
|
196
|
+
Clyde: '2EiwWnXFnvU5JabPnv8n',
|
|
197
|
+
Paul: '5Q0t7uMcjvnagumLfvZi',
|
|
198
|
+
Domi: 'AZnzlk1XvdvUeBnXmlld',
|
|
199
|
+
Dave: 'CYw3kZ02Hs0563khs1Fj',
|
|
200
|
+
Fin: 'D38z5RcWu1voky8WS1ja',
|
|
201
|
+
Sarah: 'EXAVITQu4vr4xnSDxMaL',
|
|
202
|
+
Antoni: 'ErXwobaYiN019PkySvjV',
|
|
203
|
+
Thomas: 'GBv7mTt0atIp3Br8iCZE',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a new agent
|
|
208
|
+
* Uses the same API structure as elevenlabs-client.ts
|
|
209
|
+
*/
|
|
210
|
+
async function createAgent(apiKey, config) {
|
|
211
|
+
const projectId = getProjectId();
|
|
212
|
+
const voiceId = VOICE_MAP[config.voice] || config.voiceId || config.voice || VOICE_MAP['Rachel'];
|
|
213
|
+
|
|
214
|
+
// Build ElevenLabs agent config (same structure as elevenlabs-client.ts)
|
|
215
|
+
const agentConfig = {
|
|
216
|
+
name: config.name,
|
|
217
|
+
conversation_config: {
|
|
218
|
+
agent: {
|
|
219
|
+
prompt: {
|
|
220
|
+
prompt: config.systemPrompt || '',
|
|
221
|
+
llm: 'gemini-2.0-flash',
|
|
222
|
+
temperature: config.temperature || 0.7,
|
|
223
|
+
max_tokens: 500,
|
|
224
|
+
},
|
|
225
|
+
first_message: config.greeting || 'Hello! How can I help you today?',
|
|
226
|
+
language: config.language || 'en',
|
|
227
|
+
},
|
|
228
|
+
tts: {
|
|
229
|
+
voice_id: voiceId,
|
|
230
|
+
model_id: 'eleven_flash_v2',
|
|
231
|
+
optimize_streaming_latency: 3,
|
|
232
|
+
stability: 0.5,
|
|
233
|
+
similarity_boost: 0.75,
|
|
234
|
+
speed: 1.0,
|
|
235
|
+
use_speaker_boost: true,
|
|
236
|
+
},
|
|
237
|
+
asr: {
|
|
238
|
+
quality: 'high',
|
|
239
|
+
provider: 'elevenlabs',
|
|
240
|
+
user_input_audio_format: 'pcm_16000',
|
|
241
|
+
},
|
|
242
|
+
turn: {
|
|
243
|
+
mode: 'turn',
|
|
244
|
+
turn_timeout: 7,
|
|
245
|
+
},
|
|
246
|
+
conversation: {
|
|
247
|
+
max_duration_seconds: config.maxDurationSeconds || 600,
|
|
248
|
+
silence_timeout_seconds: -1,
|
|
249
|
+
},
|
|
250
|
+
privacy: {
|
|
251
|
+
retention_days: -1,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
tags: ['lux-platform', `project-${projectId}`],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Add data collection if configured
|
|
258
|
+
if (config.dataCollection && config.dataCollection.length > 0) {
|
|
259
|
+
agentConfig.data_collection = {
|
|
260
|
+
schema: config.dataCollection.map(field => ({
|
|
261
|
+
identifier: field.identifier,
|
|
262
|
+
type: field.type || 'string',
|
|
263
|
+
description: field.description || '',
|
|
264
|
+
})),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Use /convai/agents/create endpoint (not /convai/agents)
|
|
269
|
+
const data = await elevenLabsRequest('POST', '/convai/agents/create', apiKey, agentConfig);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
id: data.agent_id,
|
|
273
|
+
name: data.name,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Update an agent
|
|
279
|
+
* Uses the same API structure as elevenlabs-client.ts
|
|
280
|
+
*/
|
|
281
|
+
async function updateAgent(apiKey, agentId, config) {
|
|
282
|
+
// First get existing agent to merge config
|
|
283
|
+
const existing = await getAgent(apiKey, agentId);
|
|
284
|
+
const voiceId = config.voice ? (VOICE_MAP[config.voice] || config.voice) : existing.voiceId;
|
|
285
|
+
|
|
286
|
+
// Build update config (same structure as elevenlabs-client.ts)
|
|
287
|
+
const updateConfig = {};
|
|
288
|
+
|
|
289
|
+
if (config.name) {
|
|
290
|
+
updateConfig.name = config.name;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Only include conversation_config if we're updating relevant fields
|
|
294
|
+
const needsConversationConfig =
|
|
295
|
+
config.systemPrompt !== undefined ||
|
|
296
|
+
config.greeting !== undefined ||
|
|
297
|
+
config.voice !== undefined ||
|
|
298
|
+
config.temperature !== undefined ||
|
|
299
|
+
config.maxDurationSeconds !== undefined ||
|
|
300
|
+
config.language !== undefined;
|
|
301
|
+
|
|
302
|
+
if (needsConversationConfig) {
|
|
303
|
+
updateConfig.conversation_config = {
|
|
304
|
+
agent: {
|
|
305
|
+
prompt: {
|
|
306
|
+
prompt: config.systemPrompt !== undefined ? config.systemPrompt : existing.systemPrompt,
|
|
307
|
+
llm: 'gemini-2.0-flash',
|
|
308
|
+
temperature: config.temperature !== undefined ? config.temperature : existing.temperature,
|
|
309
|
+
max_tokens: 500,
|
|
310
|
+
},
|
|
311
|
+
first_message: config.greeting !== undefined ? config.greeting : existing.greeting,
|
|
312
|
+
language: config.language || existing.language,
|
|
313
|
+
},
|
|
314
|
+
tts: {
|
|
315
|
+
voice_id: voiceId,
|
|
316
|
+
model_id: 'eleven_flash_v2',
|
|
317
|
+
optimize_streaming_latency: 3,
|
|
318
|
+
stability: 0.5,
|
|
319
|
+
similarity_boost: 0.75,
|
|
320
|
+
speed: 1.0,
|
|
321
|
+
use_speaker_boost: true,
|
|
322
|
+
},
|
|
323
|
+
asr: {
|
|
324
|
+
quality: 'high',
|
|
325
|
+
provider: 'elevenlabs',
|
|
326
|
+
user_input_audio_format: 'pcm_16000',
|
|
327
|
+
},
|
|
328
|
+
turn: {
|
|
329
|
+
mode: 'turn',
|
|
330
|
+
turn_timeout: 7,
|
|
331
|
+
},
|
|
332
|
+
conversation: {
|
|
333
|
+
max_duration_seconds: config.maxDurationSeconds || existing.maxDurationSeconds,
|
|
334
|
+
silence_timeout_seconds: -1,
|
|
335
|
+
},
|
|
336
|
+
privacy: {
|
|
337
|
+
retention_days: -1,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Add data collection if configured
|
|
343
|
+
if (config.dataCollection && config.dataCollection.length > 0) {
|
|
344
|
+
updateConfig.data_collection = {
|
|
345
|
+
schema: config.dataCollection.map(field => ({
|
|
346
|
+
identifier: field.identifier,
|
|
347
|
+
type: field.type || 'string',
|
|
348
|
+
description: field.description || '',
|
|
349
|
+
})),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const data = await elevenLabsRequest('PATCH', `/convai/agents/${agentId}`, apiKey, updateConfig);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
id: data.agent_id,
|
|
357
|
+
name: data.name,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Delete an agent
|
|
363
|
+
*/
|
|
364
|
+
async function deleteAgent(apiKey, agentId) {
|
|
365
|
+
await elevenLabsRequest('DELETE', `/convai/agents/${agentId}`, apiKey);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* List phone numbers
|
|
370
|
+
*/
|
|
371
|
+
async function listPhoneNumbers(apiKey) {
|
|
372
|
+
const data = await elevenLabsRequest('GET', '/convai/phone-numbers', apiKey);
|
|
373
|
+
|
|
374
|
+
return (data.phone_numbers || []).map(pn => ({
|
|
375
|
+
id: pn.phone_number_id,
|
|
376
|
+
phoneNumber: pn.phone_number,
|
|
377
|
+
label: pn.label || pn.phone_number,
|
|
378
|
+
provider: pn.provider || 'unknown',
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// =============================================================================
|
|
383
|
+
// TWILIO API FUNCTIONS (for Lux-managed inbound calls)
|
|
384
|
+
// =============================================================================
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get Twilio credentials from Lux credentials
|
|
388
|
+
*/
|
|
389
|
+
async function getTwilioCredentials() {
|
|
390
|
+
const projectId = getProjectId();
|
|
391
|
+
const studioApiUrl = getStudioApiUrl();
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const { data } = await axios.get(
|
|
395
|
+
`${studioApiUrl}/api/projects/${projectId}/credentials/by-type/twilio`,
|
|
396
|
+
{ headers: getAuthHeaders() }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (data?.credential?.data?.accountSid && data?.credential?.data?.authToken) {
|
|
400
|
+
return {
|
|
401
|
+
accountSid: data.credential.data.accountSid,
|
|
402
|
+
authToken: data.credential.data.authToken,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return null;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
// If that fails, try the dashboard API
|
|
409
|
+
try {
|
|
410
|
+
const apiUrl = getApiUrl();
|
|
411
|
+
const { data } = await axios.get(
|
|
412
|
+
`${apiUrl}/api/projects/${projectId}/credentials/by-type/twilio`,
|
|
413
|
+
{ headers: getAuthHeaders() }
|
|
414
|
+
);
|
|
415
|
+
if (data?.credential?.data?.accountSid && data?.credential?.data?.authToken) {
|
|
416
|
+
return {
|
|
417
|
+
accountSid: data.credential.data.accountSid,
|
|
418
|
+
authToken: data.credential.data.authToken,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Make Twilio API request
|
|
430
|
+
*/
|
|
431
|
+
async function twilioRequest(method, endpoint, creds, data = null) {
|
|
432
|
+
const auth = Buffer.from(`${creds.accountSid}:${creds.authToken}`).toString('base64');
|
|
433
|
+
const config = {
|
|
434
|
+
method,
|
|
435
|
+
url: `https://api.twilio.com/2010-04-01/Accounts/${creds.accountSid}${endpoint}`,
|
|
436
|
+
headers: {
|
|
437
|
+
'Authorization': `Basic ${auth}`,
|
|
438
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (data) {
|
|
443
|
+
config.data = new URLSearchParams(data).toString();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const response = await axios(config);
|
|
447
|
+
return response.data;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* List Twilio phone numbers
|
|
452
|
+
*/
|
|
453
|
+
async function listTwilioPhoneNumbers(creds) {
|
|
454
|
+
const data = await twilioRequest('GET', '/IncomingPhoneNumbers.json', creds);
|
|
455
|
+
const cache = readCache();
|
|
456
|
+
const inboundConfig = cache.inboundConfig || {};
|
|
457
|
+
|
|
458
|
+
return (data.incoming_phone_numbers || []).map(p => ({
|
|
459
|
+
sid: p.sid,
|
|
460
|
+
phoneNumber: p.phone_number,
|
|
461
|
+
friendlyName: p.friendly_name,
|
|
462
|
+
voiceUrl: p.voice_url || null,
|
|
463
|
+
isLuxManaged: p.voice_url?.includes('lux-studio-api') || false,
|
|
464
|
+
inboundConfig: inboundConfig[p.phone_number] || null,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Configure Twilio phone number for Lux-managed inbound calls
|
|
470
|
+
*/
|
|
471
|
+
async function configureTwilioInbound(creds, phoneNumber, flowId) {
|
|
472
|
+
// Find the phone number SID
|
|
473
|
+
const numbers = await listTwilioPhoneNumbers(creds);
|
|
474
|
+
const target = numbers.find(n => n.phoneNumber === phoneNumber);
|
|
475
|
+
|
|
476
|
+
if (!target) {
|
|
477
|
+
throw new Error(`Phone number ${phoneNumber} not found in your Twilio account`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Build webhook URL
|
|
481
|
+
const orgId = getOrgId();
|
|
482
|
+
const webhookUrl = `${LUX_STUDIO_API_URL}/api/webhooks/${flowId}?org=${orgId}`;
|
|
483
|
+
|
|
484
|
+
// Configure Twilio
|
|
485
|
+
await twilioRequest('POST', `/IncomingPhoneNumbers/${target.sid}.json`, creds, {
|
|
486
|
+
VoiceUrl: webhookUrl,
|
|
487
|
+
VoiceMethod: 'POST',
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Update cache
|
|
491
|
+
const cache = readCache();
|
|
492
|
+
const inboundConfig = cache.inboundConfig || {};
|
|
493
|
+
inboundConfig[phoneNumber] = {
|
|
494
|
+
flowId,
|
|
495
|
+
webhookUrl,
|
|
496
|
+
mode: 'lux-managed',
|
|
497
|
+
configuredAt: new Date().toISOString(),
|
|
498
|
+
};
|
|
499
|
+
writeCache({ inboundConfig });
|
|
500
|
+
|
|
501
|
+
return { webhookUrl, phoneNumberSid: target.sid };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Reset Twilio phone number configuration
|
|
506
|
+
*/
|
|
507
|
+
async function resetTwilioInbound(creds, phoneNumber) {
|
|
508
|
+
// Find the phone number SID
|
|
509
|
+
const numbers = await listTwilioPhoneNumbers(creds);
|
|
510
|
+
const target = numbers.find(n => n.phoneNumber === phoneNumber);
|
|
511
|
+
|
|
512
|
+
if (!target) {
|
|
513
|
+
throw new Error(`Phone number ${phoneNumber} not found in your Twilio account`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Clear webhook URL
|
|
517
|
+
await twilioRequest('POST', `/IncomingPhoneNumbers/${target.sid}.json`, creds, {
|
|
518
|
+
VoiceUrl: '',
|
|
519
|
+
StatusCallback: '',
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Update cache
|
|
523
|
+
const cache = readCache();
|
|
524
|
+
const inboundConfig = cache.inboundConfig || {};
|
|
525
|
+
delete inboundConfig[phoneNumber];
|
|
526
|
+
writeCache({ inboundConfig });
|
|
527
|
+
|
|
528
|
+
return { phoneNumberSid: target.sid };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Main handler for voice-agents commands
|
|
533
|
+
*/
|
|
534
|
+
async function handleVoiceAgents(args) {
|
|
535
|
+
// Check authentication
|
|
536
|
+
if (!isAuthenticated()) {
|
|
537
|
+
console.log(
|
|
538
|
+
chalk.red('❌ Not authenticated. Run'),
|
|
539
|
+
chalk.white('lux login'),
|
|
540
|
+
chalk.red('first.')
|
|
541
|
+
);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const command = args[0];
|
|
546
|
+
|
|
547
|
+
if (!command) {
|
|
548
|
+
console.log(`
|
|
549
|
+
${chalk.bold('Usage:')} lux voice-agents <command> [args]
|
|
550
|
+
|
|
551
|
+
${chalk.bold('Agent Commands:')}
|
|
552
|
+
list List all voice agents for current project
|
|
553
|
+
get <agent-id> Get agent details
|
|
554
|
+
create <name> [options] Create a new voice agent
|
|
555
|
+
update <agent-id> [options] Update an existing agent
|
|
556
|
+
delete <agent-id> Delete an agent
|
|
557
|
+
phones List available phone numbers
|
|
558
|
+
|
|
559
|
+
${chalk.bold('Inbound Call Commands (Lux-managed):')}
|
|
560
|
+
twilio-numbers List Twilio phone numbers with webhook status
|
|
561
|
+
configure-inbound <phone> <flow> Configure phone number for Lux-managed inbound calls
|
|
562
|
+
inbound-status <phone> Check inbound configuration status
|
|
563
|
+
reset-inbound <phone> Reset phone number configuration
|
|
564
|
+
|
|
565
|
+
${chalk.bold('Utility Commands:')}
|
|
566
|
+
status Check voice agent prerequisites (credentials, phone numbers)
|
|
567
|
+
cache Show local cache status
|
|
568
|
+
|
|
569
|
+
${chalk.bold('Create/Update Options:')}
|
|
570
|
+
--voice <name> Voice name (Rachel, Drew, Sarah, etc.)
|
|
571
|
+
--prompt <text> System prompt for the agent
|
|
572
|
+
--prompt-file <path> Read system prompt from file
|
|
573
|
+
--greeting <text> First message when call starts
|
|
574
|
+
--temperature <0-1> Response creativity (default: 0.7)
|
|
575
|
+
--max-duration <seconds> Max call length (default: 600)
|
|
576
|
+
--language <code> Language code (default: en)
|
|
577
|
+
--data-collection <json> Data fields to collect (JSON array)
|
|
578
|
+
|
|
579
|
+
${chalk.bold('Examples:')}
|
|
580
|
+
lux voice-agents list
|
|
581
|
+
lux voice-agents create "Support Agent" --voice Rachel --prompt "You are helpful."
|
|
582
|
+
lux voice-agents create "City Collector" --voice Rachel --prompt "Ask what city..." \\
|
|
583
|
+
--data-collection '[{"identifier":"city","type":"string","description":"User city"}]'
|
|
584
|
+
lux voice-agents twilio-numbers
|
|
585
|
+
lux voice-agents configure-inbound +15551234567 flow_abc123
|
|
586
|
+
lux voice-agents inbound-status +15551234567
|
|
587
|
+
|
|
588
|
+
${chalk.bold('Available Voices:')}
|
|
589
|
+
${BUILT_IN_VOICES.join(', ')}
|
|
590
|
+
`);
|
|
591
|
+
process.exit(0);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Get voice agent API key from platform credentials
|
|
595
|
+
info('Loading voice agent credentials...');
|
|
596
|
+
const apiKey = await getVoiceAgentApiKey();
|
|
597
|
+
|
|
598
|
+
if (!apiKey && command !== 'cache' && command !== 'status') {
|
|
599
|
+
error(
|
|
600
|
+
'Voice agent credentials not available.\n\n' +
|
|
601
|
+
'Voice agents may not be enabled for this project.\n' +
|
|
602
|
+
'Contact support if this issue persists.'
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
switch (command) {
|
|
608
|
+
case 'list': {
|
|
609
|
+
info('Loading voice agents...');
|
|
610
|
+
const agents = await listAgents(apiKey);
|
|
611
|
+
|
|
612
|
+
// Update cache
|
|
613
|
+
writeCache({ agents });
|
|
614
|
+
|
|
615
|
+
if (agents.length === 0) {
|
|
616
|
+
console.log('\n(No voice agents found for this project)\n');
|
|
617
|
+
console.log('Create one with: lux voice-agents create "Agent Name" --voice Rachel --prompt "..."');
|
|
618
|
+
} else {
|
|
619
|
+
console.log(`\n${chalk.bold('Voice Agents')} (${agents.length})\n`);
|
|
620
|
+
agents.forEach((a, i) => {
|
|
621
|
+
console.log(` ${chalk.cyan(a.name)}`);
|
|
622
|
+
console.log(` Voice: ${a.voice || '(default)'}`);
|
|
623
|
+
const promptPreview = (a.systemPrompt || '').substring(0, 60).replace(/\n/g, ' ');
|
|
624
|
+
if (promptPreview) {
|
|
625
|
+
console.log(` Prompt: ${promptPreview}${a.systemPrompt?.length > 60 ? '...' : ''}`);
|
|
626
|
+
}
|
|
627
|
+
console.log(` ID: ${chalk.gray(a.id)}`);
|
|
628
|
+
if (i < agents.length - 1) console.log('');
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
case 'get': {
|
|
635
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents get <agent-id>');
|
|
636
|
+
const agentId = args[1];
|
|
637
|
+
|
|
638
|
+
info(`Loading agent: ${agentId}`);
|
|
639
|
+
const agent = await getAgent(apiKey, agentId);
|
|
640
|
+
|
|
641
|
+
console.log(`\n🤖 Agent Details:\n`);
|
|
642
|
+
console.log(` ID: ${agent.id}`);
|
|
643
|
+
console.log(` Name: ${agent.name}`);
|
|
644
|
+
console.log(` Voice: ${agent.voice || '(default)'}`);
|
|
645
|
+
console.log(` Language: ${agent.language}`);
|
|
646
|
+
console.log(` Temperature: ${agent.temperature}`);
|
|
647
|
+
console.log(` Max Duration: ${agent.maxDurationSeconds}s`);
|
|
648
|
+
console.log(` Greeting: ${agent.greeting || '(none)'}`);
|
|
649
|
+
console.log(`\n📝 System Prompt:\n`);
|
|
650
|
+
console.log(agent.systemPrompt || '(empty)');
|
|
651
|
+
console.log('');
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
case 'create': {
|
|
656
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents create <name> [options]');
|
|
657
|
+
const name = args[1];
|
|
658
|
+
|
|
659
|
+
// Parse options
|
|
660
|
+
const options = parseOptions(args.slice(2));
|
|
661
|
+
|
|
662
|
+
// Read prompt from file if specified
|
|
663
|
+
if (options.promptFile) {
|
|
664
|
+
options.systemPrompt = fs.readFileSync(options.promptFile, 'utf8');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
info(`Creating voice agent: ${name}`);
|
|
668
|
+
const agent = await createAgent(apiKey, {
|
|
669
|
+
name,
|
|
670
|
+
voice: options.voice,
|
|
671
|
+
voiceId: options.voice,
|
|
672
|
+
systemPrompt: options.prompt || options.systemPrompt,
|
|
673
|
+
greeting: options.greeting,
|
|
674
|
+
temperature: options.temperature ? parseFloat(options.temperature) : undefined,
|
|
675
|
+
maxDurationSeconds: options.maxDuration ? parseInt(options.maxDuration) : undefined,
|
|
676
|
+
language: options.language,
|
|
677
|
+
dataCollection: options.dataCollection ? JSON.parse(options.dataCollection) : undefined,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// Refresh cache
|
|
681
|
+
const agents = await listAgents(apiKey);
|
|
682
|
+
writeCache({ agents });
|
|
683
|
+
|
|
684
|
+
success(`Voice agent created!`);
|
|
685
|
+
console.log(` ID: ${agent.id}`);
|
|
686
|
+
console.log(` Name: ${agent.name}`);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
case 'update': {
|
|
691
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents update <agent-id> [options]');
|
|
692
|
+
const agentId = args[1];
|
|
693
|
+
|
|
694
|
+
// Parse options
|
|
695
|
+
const options = parseOptions(args.slice(2));
|
|
696
|
+
|
|
697
|
+
// Read prompt from file if specified
|
|
698
|
+
if (options.promptFile) {
|
|
699
|
+
options.systemPrompt = fs.readFileSync(options.promptFile, 'utf8');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
info(`Updating voice agent: ${agentId}`);
|
|
703
|
+
const agent = await updateAgent(apiKey, agentId, {
|
|
704
|
+
name: options.name,
|
|
705
|
+
voice: options.voice,
|
|
706
|
+
voiceId: options.voice,
|
|
707
|
+
systemPrompt: options.prompt || options.systemPrompt,
|
|
708
|
+
greeting: options.greeting,
|
|
709
|
+
temperature: options.temperature ? parseFloat(options.temperature) : undefined,
|
|
710
|
+
maxDurationSeconds: options.maxDuration ? parseInt(options.maxDuration) : undefined,
|
|
711
|
+
language: options.language,
|
|
712
|
+
dataCollection: options.dataCollection ? JSON.parse(options.dataCollection) : undefined,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Refresh cache
|
|
716
|
+
const agents = await listAgents(apiKey);
|
|
717
|
+
writeCache({ agents });
|
|
718
|
+
|
|
719
|
+
success(`Voice agent updated!`);
|
|
720
|
+
console.log(` ID: ${agent.id}`);
|
|
721
|
+
console.log(` Name: ${agent.name}`);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
case 'delete': {
|
|
726
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents delete <agent-id>');
|
|
727
|
+
const agentId = args[1];
|
|
728
|
+
|
|
729
|
+
info(`Deleting voice agent: ${agentId}`);
|
|
730
|
+
await deleteAgent(apiKey, agentId);
|
|
731
|
+
|
|
732
|
+
// Refresh cache
|
|
733
|
+
const agents = await listAgents(apiKey);
|
|
734
|
+
writeCache({ agents });
|
|
735
|
+
|
|
736
|
+
success('Voice agent deleted!');
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
case 'phones': {
|
|
741
|
+
info('Loading phone numbers...');
|
|
742
|
+
const phoneNumbers = await listPhoneNumbers(apiKey);
|
|
743
|
+
|
|
744
|
+
// Update cache
|
|
745
|
+
writeCache({ phoneNumbers });
|
|
746
|
+
|
|
747
|
+
if (phoneNumbers.length === 0) {
|
|
748
|
+
console.log('\n(No phone numbers found)\n');
|
|
749
|
+
console.log('Import a Twilio phone number in Lux Studio Voice Agents panel.');
|
|
750
|
+
} else {
|
|
751
|
+
console.log(`\n${chalk.bold('Phone Numbers')} (${phoneNumbers.length})\n`);
|
|
752
|
+
phoneNumbers.forEach((pn, i) => {
|
|
753
|
+
console.log(` ${chalk.cyan(pn.phoneNumber)}`);
|
|
754
|
+
if (pn.label && pn.label !== pn.phoneNumber) {
|
|
755
|
+
console.log(` Label: ${pn.label}`);
|
|
756
|
+
}
|
|
757
|
+
console.log(` ID: ${chalk.gray(pn.id)}`);
|
|
758
|
+
if (i < phoneNumbers.length - 1) console.log('');
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ===========================================================================
|
|
765
|
+
// INBOUND CALL COMMANDS (Lux-managed)
|
|
766
|
+
// ===========================================================================
|
|
767
|
+
|
|
768
|
+
case 'twilio-numbers': {
|
|
769
|
+
info('Loading Twilio credentials...');
|
|
770
|
+
const twilioCreds = await getTwilioCredentials();
|
|
771
|
+
|
|
772
|
+
if (!twilioCreds) {
|
|
773
|
+
error(
|
|
774
|
+
'Twilio credentials not configured.\n\n' +
|
|
775
|
+
'Add your Twilio Account SID and Auth Token in Lux Studio:\n' +
|
|
776
|
+
' Settings → Credentials → Add Twilio Credentials'
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
info('Loading Twilio phone numbers...');
|
|
781
|
+
const twilioNumbers = await listTwilioPhoneNumbers(twilioCreds);
|
|
782
|
+
|
|
783
|
+
if (twilioNumbers.length === 0) {
|
|
784
|
+
console.log('\n(No phone numbers found in your Twilio account)\n');
|
|
785
|
+
} else {
|
|
786
|
+
console.log(`\n${chalk.bold('Twilio Phone Numbers')} (${twilioNumbers.length})\n`);
|
|
787
|
+
twilioNumbers.forEach((n, i) => {
|
|
788
|
+
const status = n.isLuxManaged
|
|
789
|
+
? chalk.green('Lux-managed')
|
|
790
|
+
: n.voiceUrl
|
|
791
|
+
? chalk.yellow('External webhook')
|
|
792
|
+
: chalk.gray('Not configured');
|
|
793
|
+
console.log(` ${chalk.cyan(n.phoneNumber)} ${status}`);
|
|
794
|
+
if (n.friendlyName && n.friendlyName !== n.phoneNumber) {
|
|
795
|
+
console.log(` Name: ${n.friendlyName}`);
|
|
796
|
+
}
|
|
797
|
+
if (n.inboundConfig) {
|
|
798
|
+
console.log(` Flow: ${n.inboundConfig.flowId}`);
|
|
799
|
+
}
|
|
800
|
+
console.log(` SID: ${chalk.gray(n.sid)}`);
|
|
801
|
+
if (i < twilioNumbers.length - 1) console.log('');
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
case 'configure-inbound': {
|
|
808
|
+
requireArgs(args.slice(1), 2, 'lux voice-agents configure-inbound <phone-number> <flow-id>');
|
|
809
|
+
const phoneNumber = args[1];
|
|
810
|
+
const flowId = args[2];
|
|
811
|
+
|
|
812
|
+
info('Loading Twilio credentials...');
|
|
813
|
+
const twilioCreds = await getTwilioCredentials();
|
|
814
|
+
|
|
815
|
+
if (!twilioCreds) {
|
|
816
|
+
error(
|
|
817
|
+
'Twilio credentials not configured.\n\n' +
|
|
818
|
+
'Add your Twilio Account SID and Auth Token in Lux Studio:\n' +
|
|
819
|
+
' Settings → Credentials → Add Twilio Credentials'
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
info(`Configuring ${phoneNumber} for flow ${flowId}...`);
|
|
824
|
+
const result = await configureTwilioInbound(twilioCreds, phoneNumber, flowId);
|
|
825
|
+
|
|
826
|
+
success('Phone number configured for Lux-managed inbound calls!');
|
|
827
|
+
console.log(`\n Phone Number: ${phoneNumber}`);
|
|
828
|
+
console.log(` Flow ID: ${flowId}`);
|
|
829
|
+
console.log(` Webhook URL: ${result.webhookUrl}`);
|
|
830
|
+
console.log('');
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
case 'inbound-status': {
|
|
835
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents inbound-status <phone-number>');
|
|
836
|
+
const phoneNumber = args[1];
|
|
837
|
+
|
|
838
|
+
info('Loading Twilio credentials...');
|
|
839
|
+
const twilioCreds = await getTwilioCredentials();
|
|
840
|
+
|
|
841
|
+
if (!twilioCreds) {
|
|
842
|
+
error(
|
|
843
|
+
'Twilio credentials not configured.\n\n' +
|
|
844
|
+
'Add your Twilio Account SID and Auth Token in Lux Studio:\n' +
|
|
845
|
+
' Settings → Credentials → Add Twilio Credentials'
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
info(`Checking status for ${phoneNumber}...`);
|
|
850
|
+
const numbers = await listTwilioPhoneNumbers(twilioCreds);
|
|
851
|
+
const target = numbers.find(n => n.phoneNumber === phoneNumber);
|
|
852
|
+
|
|
853
|
+
if (!target) {
|
|
854
|
+
error(`Phone number ${phoneNumber} not found in your Twilio account`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
console.log(`\n📞 Inbound Status for ${phoneNumber}\n`);
|
|
858
|
+
console.log(` SID: ${target.sid}`);
|
|
859
|
+
console.log(` Friendly Name: ${target.friendlyName}`);
|
|
860
|
+
console.log(` Voice URL: ${target.voiceUrl || '(not configured)'}`);
|
|
861
|
+
console.log(` Lux-managed: ${target.isLuxManaged ? 'Yes' : 'No'}`);
|
|
862
|
+
|
|
863
|
+
if (target.inboundConfig) {
|
|
864
|
+
console.log(`\n Cached Config:`);
|
|
865
|
+
console.log(` Flow ID: ${target.inboundConfig.flowId}`);
|
|
866
|
+
console.log(` Webhook URL: ${target.inboundConfig.webhookUrl}`);
|
|
867
|
+
console.log(` Configured At: ${target.inboundConfig.configuredAt}`);
|
|
868
|
+
}
|
|
869
|
+
console.log('');
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
case 'reset-inbound': {
|
|
874
|
+
requireArgs(args.slice(1), 1, 'lux voice-agents reset-inbound <phone-number>');
|
|
875
|
+
const phoneNumber = args[1];
|
|
876
|
+
|
|
877
|
+
info('Loading Twilio credentials...');
|
|
878
|
+
const twilioCreds = await getTwilioCredentials();
|
|
879
|
+
|
|
880
|
+
if (!twilioCreds) {
|
|
881
|
+
error(
|
|
882
|
+
'Twilio credentials not configured.\n\n' +
|
|
883
|
+
'Add your Twilio Account SID and Auth Token in Lux Studio:\n' +
|
|
884
|
+
' Settings → Credentials → Add Twilio Credentials'
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
info(`Resetting configuration for ${phoneNumber}...`);
|
|
889
|
+
await resetTwilioInbound(twilioCreds, phoneNumber);
|
|
890
|
+
|
|
891
|
+
success('Phone number configuration reset!');
|
|
892
|
+
console.log(`\n Phone Number: ${phoneNumber}`);
|
|
893
|
+
console.log(' Voice URL has been cleared.');
|
|
894
|
+
console.log('');
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ===========================================================================
|
|
899
|
+
// UTILITY COMMANDS
|
|
900
|
+
// ===========================================================================
|
|
901
|
+
|
|
902
|
+
case 'status': {
|
|
903
|
+
console.log(`\n${chalk.bold('Voice Agent Prerequisites Status')}\n`);
|
|
904
|
+
|
|
905
|
+
// Check ElevenLabs/Voice Agent credentials
|
|
906
|
+
const elevenLabsOk = !!apiKey;
|
|
907
|
+
console.log(`ElevenLabs API: ${elevenLabsOk ? chalk.green('✓ configured') : chalk.red('✗ not configured')}`);
|
|
908
|
+
|
|
909
|
+
// Check Twilio credentials
|
|
910
|
+
let twilioOk = false;
|
|
911
|
+
let twilioNumbers = [];
|
|
912
|
+
try {
|
|
913
|
+
const twilioCreds = await getTwilioCredentials();
|
|
914
|
+
twilioOk = !!twilioCreds;
|
|
915
|
+
if (twilioOk) {
|
|
916
|
+
twilioNumbers = await listTwilioPhoneNumbers(twilioCreds);
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
919
|
+
twilioOk = false;
|
|
920
|
+
}
|
|
921
|
+
console.log(`Twilio API: ${twilioOk ? chalk.green('✓ configured') : chalk.red('✗ not configured')}`);
|
|
922
|
+
|
|
923
|
+
// Check phone numbers
|
|
924
|
+
let importedNumbers = [];
|
|
925
|
+
if (elevenLabsOk) {
|
|
926
|
+
try {
|
|
927
|
+
importedNumbers = await listPhoneNumbers(apiKey);
|
|
928
|
+
} catch {
|
|
929
|
+
// Ignore errors
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const totalPhones = Math.max(twilioNumbers.length, importedNumbers.length);
|
|
934
|
+
console.log(`Phone Numbers: ${totalPhones > 0 ? chalk.green(`✓ ${totalPhones} available`) : chalk.yellow('⚠ none available')}`);
|
|
935
|
+
|
|
936
|
+
// Summary and next steps
|
|
937
|
+
console.log('');
|
|
938
|
+
|
|
939
|
+
if (!elevenLabsOk) {
|
|
940
|
+
console.log(chalk.yellow('⚠ Voice agent credentials not available.'));
|
|
941
|
+
console.log(' Voice agents may not be enabled for this project.');
|
|
942
|
+
console.log('');
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!twilioOk) {
|
|
946
|
+
console.log(chalk.yellow('⚠ Twilio not configured - inbound calls will not work.'));
|
|
947
|
+
console.log(' Add credentials in: Settings → Credentials → Twilio');
|
|
948
|
+
console.log('');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (totalPhones === 0 && twilioOk) {
|
|
952
|
+
console.log(chalk.yellow('⚠ No phone numbers available.'));
|
|
953
|
+
console.log(' Import a Twilio number in the Voice Agents panel.');
|
|
954
|
+
console.log('');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Ready status
|
|
958
|
+
const outboundReady = elevenLabsOk && totalPhones > 0;
|
|
959
|
+
const inboundReady = elevenLabsOk && twilioOk && totalPhones > 0;
|
|
960
|
+
|
|
961
|
+
console.log(chalk.bold('Capabilities:'));
|
|
962
|
+
console.log(` Outbound calls: ${outboundReady ? chalk.green('✓ ready') : chalk.red('✗ not ready')}`);
|
|
963
|
+
console.log(` Inbound calls: ${inboundReady ? chalk.green('✓ ready') : chalk.red('✗ not ready')}`);
|
|
964
|
+
console.log('');
|
|
965
|
+
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
case 'cache': {
|
|
970
|
+
const cache = readCache();
|
|
971
|
+
const cachePath = getCachePath();
|
|
972
|
+
|
|
973
|
+
console.log(`\n📁 Cache Location: ${cachePath || '(not available)'}\n`);
|
|
974
|
+
|
|
975
|
+
if (cache.lastUpdated) {
|
|
976
|
+
console.log(`Last Updated: ${cache.lastUpdated}\n`);
|
|
977
|
+
console.log(`Agents: ${cache.agents?.length || 0}`);
|
|
978
|
+
console.log(`Phone Numbers: ${cache.phoneNumbers?.length || 0}`);
|
|
979
|
+
console.log(`Inbound Configs: ${Object.keys(cache.inboundConfig || {}).length}`);
|
|
980
|
+
|
|
981
|
+
if (cache.agents?.length > 0) {
|
|
982
|
+
console.log(`\nAgents:`);
|
|
983
|
+
cache.agents.forEach(a => {
|
|
984
|
+
console.log(` - ${a.name} (${a.id})`);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (cache.phoneNumbers?.length > 0) {
|
|
989
|
+
console.log(`\nPhone Numbers:`);
|
|
990
|
+
cache.phoneNumbers.forEach(p => {
|
|
991
|
+
console.log(` - ${p.phoneNumber} (${p.id})`);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (cache.inboundConfig && Object.keys(cache.inboundConfig).length > 0) {
|
|
996
|
+
console.log(`\nInbound Configs (Lux-managed):`);
|
|
997
|
+
Object.entries(cache.inboundConfig).forEach(([phone, config]) => {
|
|
998
|
+
console.log(` - ${phone}`);
|
|
999
|
+
console.log(` Flow: ${config.flowId}`);
|
|
1000
|
+
console.log(` Configured: ${config.configuredAt}`);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
console.log('Cache is empty. Run "lux voice-agents list" to populate.');
|
|
1005
|
+
}
|
|
1006
|
+
console.log('');
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
default:
|
|
1011
|
+
error(
|
|
1012
|
+
`Unknown command: ${command}\n\nRun 'lux voice-agents' to see available commands`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
const errorMessage =
|
|
1017
|
+
err.response?.data?.detail?.message ||
|
|
1018
|
+
err.response?.data?.detail ||
|
|
1019
|
+
err.response?.data?.error ||
|
|
1020
|
+
err.message ||
|
|
1021
|
+
'Unknown error';
|
|
1022
|
+
error(`${errorMessage}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Parse command line options (--key value format)
|
|
1028
|
+
*/
|
|
1029
|
+
function parseOptions(args) {
|
|
1030
|
+
const options = {};
|
|
1031
|
+
let i = 0;
|
|
1032
|
+
|
|
1033
|
+
while (i < args.length) {
|
|
1034
|
+
const arg = args[i];
|
|
1035
|
+
|
|
1036
|
+
if (arg.startsWith('--')) {
|
|
1037
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1038
|
+
const value = args[i + 1];
|
|
1039
|
+
|
|
1040
|
+
if (value && !value.startsWith('--')) {
|
|
1041
|
+
options[key] = value;
|
|
1042
|
+
i += 2;
|
|
1043
|
+
} else {
|
|
1044
|
+
options[key] = true;
|
|
1045
|
+
i += 1;
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
i += 1;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return options;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
module.exports = { handleVoiceAgents };
|