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.
@@ -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 };