@elizaos/plugin-twilio 2.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1650 @@
1
+ // src/index.ts
2
+ import { logger as logger7 } from "@elizaos/core";
3
+
4
+ // src/actions/sendSms.ts
5
+ import {
6
+ logger as logger2
7
+ } from "@elizaos/core";
8
+
9
+ // src/constants.ts
10
+ var MESSAGE_CONSTANTS = {
11
+ MAX_MESSAGES: 10,
12
+ RECENT_MESSAGE_COUNT: 3,
13
+ CHAT_HISTORY_COUNT: 5,
14
+ INTEREST_DECAY_TIME: 5 * 60 * 1e3,
15
+ // 5 minutes
16
+ PARTIAL_INTEREST_DECAY: 3 * 60 * 1e3,
17
+ // 3 minutes
18
+ DEFAULT_SIMILARITY_THRESHOLD: 0.3,
19
+ DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.2
20
+ };
21
+ var TWILIO_SERVICE_NAME = "twilio";
22
+ var TWILIO_CONSTANTS = {
23
+ // Message limits
24
+ SMS_MAX_LENGTH: 1600,
25
+ MMS_MAX_MEDIA_SIZE: 5242880,
26
+ // 5MB
27
+ MMS_MAX_MEDIA_COUNT: 10,
28
+ // Voice constants
29
+ DEFAULT_VOICE: "alice",
30
+ DEFAULT_LANGUAGE: "en-US",
31
+ VOICE_STREAM_TIMEOUT: 3e5,
32
+ // 5 minutes
33
+ // API endpoints
34
+ API_BASE_URL: "https://api.twilio.com",
35
+ // Webhook paths
36
+ WEBHOOK_PATHS: {
37
+ SMS: "/webhooks/twilio/sms",
38
+ VOICE: "/webhooks/twilio/voice",
39
+ STATUS: "/webhooks/twilio/status",
40
+ VOICE_STREAM: "/webhooks/twilio/voice-stream"
41
+ },
42
+ // TwiML templates
43
+ TWIML: {
44
+ DEFAULT_VOICE_RESPONSE: `<?xml version="1.0" encoding="UTF-8"?>
45
+ <Response>
46
+ <Say voice="alice">Hello from Eliza AI assistant. How can I help you today?</Say>
47
+ <Pause length="1"/>
48
+ </Response>`,
49
+ STREAM_RESPONSE: (streamUrl) => `<?xml version="1.0" encoding="UTF-8"?>
50
+ <Response>
51
+ <Start>
52
+ <Stream url="${streamUrl}" />
53
+ </Start>
54
+ <Say>Please wait while I connect you to the AI assistant.</Say>
55
+ <Pause length="60"/>
56
+ </Response>`
57
+ },
58
+ // Rate limits
59
+ RATE_LIMITS: {
60
+ SMS_PER_SECOND: 1,
61
+ CALLS_PER_SECOND: 1,
62
+ WEBHOOK_TIMEOUT: 15e3
63
+ // 15 seconds
64
+ },
65
+ // Cache TTL
66
+ CACHE_TTL: {
67
+ CONVERSATION: 3600,
68
+ // 1 hour
69
+ MEDIA: 86400,
70
+ // 24 hours
71
+ CALL_STATE: 1800
72
+ // 30 minutes
73
+ }
74
+ };
75
+ var ERROR_MESSAGES = {
76
+ INVALID_PHONE_NUMBER: "Invalid phone number format. Please use E.164 format (e.g., +18885551212)",
77
+ MISSING_CREDENTIALS: "Twilio credentials not configured",
78
+ WEBHOOK_VALIDATION_FAILED: "Failed to validate Twilio webhook signature",
79
+ RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please try again later.",
80
+ MEDIA_TOO_LARGE: "Media file too large. Maximum size is 5MB",
81
+ UNSUPPORTED_MEDIA_TYPE: "Unsupported media type"
82
+ };
83
+
84
+ // src/utils.ts
85
+ import { logger } from "@elizaos/core";
86
+ function validatePhoneNumber(phoneNumber) {
87
+ const e164Regex = /^\+[1-9]\d{1,14}$/;
88
+ return e164Regex.test(phoneNumber);
89
+ }
90
+ function validateMessagingAddress(address) {
91
+ const messagingRegex = /^(whatsapp:)?\+[1-9]\d{1,14}$/;
92
+ return messagingRegex.test(address);
93
+ }
94
+ function isWhatsAppAddress(address) {
95
+ return address.startsWith("whatsapp:");
96
+ }
97
+ function stripWhatsAppPrefix(address) {
98
+ return isWhatsAppAddress(address) ? address.slice("whatsapp:".length) : address;
99
+ }
100
+ function formatMessagingAddress(address, defaultCountryCode = "+1") {
101
+ const hasPrefix = isWhatsAppAddress(address);
102
+ const formatted = formatPhoneNumber(
103
+ stripWhatsAppPrefix(address),
104
+ defaultCountryCode
105
+ );
106
+ if (!formatted) {
107
+ return null;
108
+ }
109
+ return hasPrefix ? `whatsapp:${formatted}` : formatted;
110
+ }
111
+ function formatPhoneNumber(phoneNumber, defaultCountryCode = "+1") {
112
+ let cleaned = phoneNumber.replace(/[^\d+]/g, "");
113
+ if (!cleaned.startsWith("+")) {
114
+ if (cleaned.startsWith("1") && cleaned.length === 11) {
115
+ cleaned = "+" + cleaned;
116
+ } else if (cleaned.length === 10) {
117
+ cleaned = defaultCountryCode + cleaned;
118
+ } else {
119
+ return null;
120
+ }
121
+ }
122
+ if (validatePhoneNumber(cleaned)) {
123
+ return cleaned;
124
+ }
125
+ return null;
126
+ }
127
+ var generateTwiML = {
128
+ /**
129
+ * Generates TwiML for a simple voice response
130
+ * @param message The message to say
131
+ * @param voice The voice to use (default: alice)
132
+ * @returns TwiML string
133
+ */
134
+ say: (message, voice = "alice") => {
135
+ return `<?xml version="1.0" encoding="UTF-8"?>
136
+ <Response>
137
+ <Say voice="${voice}">${escapeXml(message)}</Say>
138
+ </Response>`;
139
+ },
140
+ /**
141
+ * Generates TwiML for gathering input
142
+ * @param prompt The prompt message
143
+ * @param options Gather options
144
+ * @returns TwiML string
145
+ */
146
+ gather: (prompt, options = {}) => {
147
+ const {
148
+ numDigits = 1,
149
+ timeout = 5,
150
+ action = "",
151
+ method = "POST"
152
+ } = options;
153
+ return `<?xml version="1.0" encoding="UTF-8"?>
154
+ <Response>
155
+ <Gather numDigits="${numDigits}" timeout="${timeout}" action="${action}" method="${method}">
156
+ <Say>${escapeXml(prompt)}</Say>
157
+ </Gather>
158
+ </Response>`;
159
+ },
160
+ /**
161
+ * Generates TwiML for starting a media stream
162
+ * @param streamUrl The WebSocket URL for streaming
163
+ * @param customParameters Optional custom parameters
164
+ * @returns TwiML string
165
+ */
166
+ stream: (streamUrl, customParameters) => {
167
+ let params = "";
168
+ if (customParameters) {
169
+ params = Object.entries(customParameters).map(
170
+ ([key, value]) => `<Parameter name="${key}" value="${escapeXml(value)}" />`
171
+ ).join("\n ");
172
+ }
173
+ return `<?xml version="1.0" encoding="UTF-8"?>
174
+ <Response>
175
+ <Start>
176
+ <Stream url="${streamUrl}">
177
+ ${params}
178
+ </Stream>
179
+ </Start>
180
+ <Say>Please wait while I connect you to the AI assistant.</Say>
181
+ <Pause length="60"/>
182
+ </Response>`;
183
+ },
184
+ /**
185
+ * Generates TwiML for recording a call
186
+ * @param options Recording options
187
+ * @returns TwiML string
188
+ */
189
+ record: (options = {}) => {
190
+ const {
191
+ maxLength = 3600,
192
+ timeout = 5,
193
+ transcribe = false,
194
+ action = ""
195
+ } = options;
196
+ return `<?xml version="1.0" encoding="UTF-8"?>
197
+ <Response>
198
+ <Record maxLength="${maxLength}" timeout="${timeout}" transcribe="${transcribe}" action="${action}" />
199
+ </Response>`;
200
+ },
201
+ /**
202
+ * Generates TwiML to hang up
203
+ * @returns TwiML string
204
+ */
205
+ hangup: () => {
206
+ return `<?xml version="1.0" encoding="UTF-8"?>
207
+ <Response>
208
+ <Hangup />
209
+ </Response>`;
210
+ }
211
+ };
212
+ function escapeXml(str) {
213
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
214
+ }
215
+ function extractPhoneNumber(input) {
216
+ const wantsWhatsApp = input.toLowerCase().includes("whatsapp:");
217
+ const patterns = [
218
+ /\+\d{1,15}/,
219
+ // E.164 format
220
+ /\(\d{3}\)\s*\d{3}-\d{4}/,
221
+ // (555) 555-5555
222
+ /\d{3}-\d{3}-\d{4}/,
223
+ // 555-555-5555
224
+ /\d{10,15}/
225
+ // Just digits
226
+ ];
227
+ for (const pattern of patterns) {
228
+ const match = input.match(pattern);
229
+ if (match) {
230
+ const formatted = formatPhoneNumber(match[0]);
231
+ if (formatted) {
232
+ return wantsWhatsApp ? `whatsapp:${formatted}` : formatted;
233
+ }
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+ function chunkTextForSms(text, maxLength = 160) {
239
+ if (text.length <= maxLength) {
240
+ return [text];
241
+ }
242
+ const chunks = [];
243
+ const words = text.split(" ");
244
+ let currentChunk = "";
245
+ for (const word of words) {
246
+ if ((currentChunk + " " + word).trim().length <= maxLength) {
247
+ currentChunk = (currentChunk + " " + word).trim();
248
+ } else {
249
+ if (currentChunk) {
250
+ chunks.push(currentChunk);
251
+ }
252
+ currentChunk = word;
253
+ }
254
+ }
255
+ if (currentChunk) {
256
+ chunks.push(currentChunk);
257
+ }
258
+ return chunks;
259
+ }
260
+
261
+ // src/actions/sendSms.ts
262
+ var sendSmsAction = {
263
+ name: "SEND_SMS",
264
+ description: "Send an SMS message to a phone number via Twilio",
265
+ validate: async (runtime, message, state) => {
266
+ const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
267
+ if (!twilioService) {
268
+ logger2.error("Twilio service not found");
269
+ return false;
270
+ }
271
+ const text = message.content.text || "";
272
+ const phoneNumber = extractPhoneNumber(text);
273
+ const hasSmsIntent = text.toLowerCase().includes("sms") || text.toLowerCase().includes("text") || text.toLowerCase().includes("message");
274
+ return !!phoneNumber && hasSmsIntent;
275
+ },
276
+ handler: async (runtime, message, state, options, callback) => {
277
+ try {
278
+ const twilioService = runtime.getService(
279
+ TWILIO_SERVICE_NAME
280
+ );
281
+ if (!twilioService) {
282
+ throw new Error("Twilio service not available");
283
+ }
284
+ const text = message.content.text || "";
285
+ const phoneNumber = extractPhoneNumber(text);
286
+ if (!phoneNumber) {
287
+ throw new Error("No phone number found in message");
288
+ }
289
+ let messageContent = text.replace(phoneNumber, "").replace(
290
+ /^(send\s*(an?\s*)?(sms|text|message)\s*(to\s*)?|text\s+)/gi,
291
+ ""
292
+ ).replace(/\b(saying|with\s*(the\s*)?(message|text))\b/gi, "").trim();
293
+ messageContent = messageContent.replace(/^["']|["']$/g, "").trim();
294
+ if (!messageContent) {
295
+ messageContent = "Hello from your AI assistant!";
296
+ }
297
+ if (!validateMessagingAddress(phoneNumber)) {
298
+ throw new Error("Invalid phone number format");
299
+ }
300
+ const messageChunks = chunkTextForSms(messageContent);
301
+ for (const chunk of messageChunks) {
302
+ await twilioService.sendSms(phoneNumber, chunk);
303
+ logger2.info(`SMS sent to ${phoneNumber}: ${chunk.substring(0, 50)}...`);
304
+ }
305
+ if (callback) {
306
+ callback({
307
+ text: `SMS message sent successfully to ${phoneNumber}`,
308
+ success: true
309
+ });
310
+ }
311
+ } catch (error) {
312
+ logger2.error({ error: String(error) }, "Error sending SMS");
313
+ if (callback) {
314
+ callback({
315
+ text: `Failed to send SMS: ${error instanceof Error ? error.message : "Unknown error"}`,
316
+ success: false
317
+ });
318
+ }
319
+ }
320
+ },
321
+ examples: [
322
+ [
323
+ {
324
+ name: "user",
325
+ content: {
326
+ text: "Send an SMS to +18885551234 saying 'Hello from AI assistant!'"
327
+ }
328
+ },
329
+ {
330
+ name: "assistant",
331
+ content: {
332
+ text: "I'll send that SMS message for you.",
333
+ action: "SEND_SMS"
334
+ }
335
+ }
336
+ ],
337
+ [
338
+ {
339
+ name: "user",
340
+ content: {
341
+ text: "Text +15555551234 with the message 'Meeting confirmed for 3pm tomorrow'"
342
+ }
343
+ },
344
+ {
345
+ name: "assistant",
346
+ content: {
347
+ text: "Sending the meeting confirmation SMS now.",
348
+ action: "SEND_SMS"
349
+ }
350
+ }
351
+ ]
352
+ ],
353
+ similes: [
354
+ "send sms",
355
+ "send text",
356
+ "text message",
357
+ "sms to",
358
+ "text to",
359
+ "message phone"
360
+ ]
361
+ };
362
+ var sendSms_default = sendSmsAction;
363
+
364
+ // src/actions/makeCall.ts
365
+ import {
366
+ logger as logger3
367
+ } from "@elizaos/core";
368
+ var makeCallAction = {
369
+ name: "MAKE_CALL",
370
+ description: "Make a phone call via Twilio with a message or custom TwiML",
371
+ validate: async (runtime, message, state) => {
372
+ const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
373
+ if (!twilioService) {
374
+ logger3.error("Twilio service not found");
375
+ return false;
376
+ }
377
+ const text = message.content.text || "";
378
+ const phoneNumber = extractPhoneNumber(text);
379
+ const callIntent = text.toLowerCase().includes("call") || text.toLowerCase().includes("phone") || text.toLowerCase().includes("dial");
380
+ return !!phoneNumber && callIntent;
381
+ },
382
+ handler: async (runtime, message, state, options, callback) => {
383
+ try {
384
+ const twilioService = runtime.getService(
385
+ TWILIO_SERVICE_NAME
386
+ );
387
+ if (!twilioService) {
388
+ throw new Error("Twilio service not available");
389
+ }
390
+ const text = message.content.text || "";
391
+ const phoneNumber = extractPhoneNumber(text);
392
+ if (!phoneNumber) {
393
+ throw new Error("No phone number found in message");
394
+ }
395
+ let callMessage = text.replace(phoneNumber, "").replace(/call|phone|dial/gi, "").replace(/and\s*say/gi, "").replace(/with\s*(the\s*)?(message|saying)/gi, "").trim();
396
+ callMessage = callMessage.replace(/^["']|["']$/g, "").trim();
397
+ if (!callMessage) {
398
+ callMessage = "Hello, this is an automated call from your AI assistant.";
399
+ }
400
+ if (!validatePhoneNumber(phoneNumber)) {
401
+ throw new Error("Invalid phone number format");
402
+ }
403
+ const twiml = generateTwiML.say(callMessage);
404
+ const call = await twilioService.makeCall(phoneNumber, twiml);
405
+ logger3.info(`Call initiated to ${phoneNumber}, Call SID: ${call.sid}`);
406
+ if (callback) {
407
+ callback({
408
+ text: `Call initiated successfully to ${phoneNumber}. Call ID: ${call.sid}`,
409
+ success: true
410
+ });
411
+ }
412
+ } catch (error) {
413
+ logger3.error({ error: String(error) }, "Error making call");
414
+ if (callback) {
415
+ callback({
416
+ text: `Failed to make call: ${error instanceof Error ? error.message : "Unknown error"}`,
417
+ success: false
418
+ });
419
+ }
420
+ }
421
+ },
422
+ examples: [
423
+ [
424
+ {
425
+ name: "user",
426
+ content: {
427
+ text: "Call +18885551234 and say 'This is an important reminder about your appointment tomorrow at 3pm'"
428
+ }
429
+ },
430
+ {
431
+ name: "assistant",
432
+ content: {
433
+ text: "I'll place that call with your reminder message.",
434
+ action: "MAKE_CALL"
435
+ }
436
+ }
437
+ ],
438
+ [
439
+ {
440
+ name: "user",
441
+ content: {
442
+ text: "Phone +15555551234 with an automated message about the meeting cancellation"
443
+ }
444
+ },
445
+ {
446
+ name: "assistant",
447
+ content: {
448
+ text: "Initiating the call with the cancellation message.",
449
+ action: "MAKE_CALL"
450
+ }
451
+ }
452
+ ]
453
+ ],
454
+ similes: [
455
+ "make call",
456
+ "phone call",
457
+ "call phone",
458
+ "dial number",
459
+ "voice call",
460
+ "ring phone"
461
+ ]
462
+ };
463
+ var makeCall_default = makeCallAction;
464
+
465
+ // src/actions/sendMms.ts
466
+ import {
467
+ logger as logger4
468
+ } from "@elizaos/core";
469
+ var sendMmsAction = {
470
+ name: "SEND_MMS",
471
+ description: "Send an MMS (multimedia message) with images, audio, or video via Twilio",
472
+ validate: async (runtime, message, state) => {
473
+ const twilioService = runtime.getService(TWILIO_SERVICE_NAME);
474
+ if (!twilioService) {
475
+ logger4.error("Twilio service not found");
476
+ return false;
477
+ }
478
+ const text = message.content.text || "";
479
+ const phoneNumber = extractPhoneNumber(text);
480
+ const mediaIntent = text.toLowerCase().includes("image") || text.toLowerCase().includes("photo") || text.toLowerCase().includes("picture") || text.toLowerCase().includes("video") || text.toLowerCase().includes("media") || text.toLowerCase().includes("mms");
481
+ const urlPattern = /https?:\/\/[^\s]+/g;
482
+ const hasUrls = urlPattern.test(text);
483
+ return !!phoneNumber && (mediaIntent || hasUrls);
484
+ },
485
+ handler: async (runtime, message, state, options, callback) => {
486
+ try {
487
+ const twilioService = runtime.getService(
488
+ TWILIO_SERVICE_NAME
489
+ );
490
+ if (!twilioService) {
491
+ throw new Error("Twilio service not available");
492
+ }
493
+ const text = message.content.text || "";
494
+ const phoneNumber = extractPhoneNumber(text);
495
+ if (!phoneNumber) {
496
+ throw new Error("No phone number found in message");
497
+ }
498
+ const urlPattern = /https?:\/\/[^\s]+/g;
499
+ const mediaUrls = text.match(urlPattern) || [];
500
+ if (mediaUrls.length === 0) {
501
+ mediaUrls.push("https://demo.twilio.com/owl.png");
502
+ }
503
+ let messageContent = text.replace(phoneNumber, "").replace(
504
+ /send\s*(an?\s*)?(mms|picture|photo|image|video|media)\s*(to\s*)?/gi,
505
+ ""
506
+ ).replace(/with\s*(the\s*)?(image|photo|picture|video|media|mms)/gi, "").replace(/at|from/gi, "").replace(urlPattern, "").replace(/saying|with\s*(the\s*)?(message|text)/gi, "").trim();
507
+ messageContent = messageContent.replace(/^["']|["']$/g, "").trim();
508
+ if (!messageContent) {
509
+ messageContent = "Here's the media you requested";
510
+ }
511
+ if (!validateMessagingAddress(phoneNumber)) {
512
+ throw new Error("Invalid phone number format");
513
+ }
514
+ const sentMessage = await twilioService.sendSms(
515
+ phoneNumber,
516
+ messageContent,
517
+ mediaUrls
518
+ );
519
+ logger4.info(
520
+ `MMS sent to ${phoneNumber} with ${mediaUrls.length} media attachment(s)`
521
+ );
522
+ if (callback) {
523
+ callback({
524
+ text: `MMS sent successfully to ${phoneNumber} with ${mediaUrls.length} media attachment(s)`,
525
+ success: true
526
+ });
527
+ }
528
+ } catch (error) {
529
+ logger4.error({ error: String(error) }, "Error sending MMS");
530
+ if (callback) {
531
+ callback({
532
+ text: `Failed to send MMS: ${error instanceof Error ? error.message : "Unknown error"}`,
533
+ success: false
534
+ });
535
+ }
536
+ }
537
+ },
538
+ examples: [
539
+ [
540
+ {
541
+ name: "user",
542
+ content: {
543
+ text: "Send a picture to +18885551234 with the image from https://example.com/photo.jpg saying 'Check out this photo!'"
544
+ }
545
+ },
546
+ {
547
+ name: "assistant",
548
+ content: {
549
+ text: "I'll send that photo with your message.",
550
+ action: "SEND_MMS"
551
+ }
552
+ }
553
+ ],
554
+ [
555
+ {
556
+ name: "user",
557
+ content: {
558
+ text: "Send an MMS to +15555551234 with the video at https://example.com/video.mp4"
559
+ }
560
+ },
561
+ {
562
+ name: "assistant",
563
+ content: {
564
+ text: "Sending the video message now.",
565
+ action: "SEND_MMS"
566
+ }
567
+ }
568
+ ]
569
+ ],
570
+ similes: [
571
+ "send mms",
572
+ "send picture",
573
+ "send photo",
574
+ "send image",
575
+ "send video",
576
+ "send media",
577
+ "picture message",
578
+ "photo message"
579
+ ]
580
+ };
581
+ var sendMms_default = sendMmsAction;
582
+
583
+ // src/providers/conversationHistory.ts
584
+ var conversationHistoryProvider = {
585
+ name: "twilioConversationHistory",
586
+ description: "Provides recent SMS/MMS conversation history with a phone number",
587
+ get: async (runtime, message, state) => {
588
+ try {
589
+ const twilioService = runtime.getService(
590
+ TWILIO_SERVICE_NAME
591
+ );
592
+ if (!twilioService) {
593
+ return {
594
+ text: "No Twilio conversation history available - service not initialized"
595
+ };
596
+ }
597
+ if (typeof message.content === "string") {
598
+ return {
599
+ text: "No phone number found in context"
600
+ };
601
+ }
602
+ const phoneNumber = message.content.phoneNumber || message.content.text?.match(/\+?\d{10,15}/)?.[0];
603
+ if (!phoneNumber || typeof phoneNumber !== "string") {
604
+ return {
605
+ text: "No phone number found in context"
606
+ };
607
+ }
608
+ const conversationHistory = twilioService.getConversationHistory(
609
+ phoneNumber,
610
+ 10
611
+ );
612
+ if (!conversationHistory || conversationHistory.length === 0) {
613
+ return {
614
+ text: `No recent conversation history with ${phoneNumber}`
615
+ };
616
+ }
617
+ const history = conversationHistory.map((msg) => {
618
+ const direction = msg.direction === "inbound" ? "From" : "To";
619
+ const time = new Date(msg.dateCreated).toLocaleString();
620
+ return `[${time}] ${direction} ${phoneNumber}: ${msg.body}`;
621
+ }).join("\n");
622
+ return {
623
+ text: `Recent SMS conversation with ${phoneNumber}:
624
+ ${history}`,
625
+ data: {
626
+ phoneNumber,
627
+ messageCount: conversationHistory.length,
628
+ lastMessage: conversationHistory[conversationHistory.length - 1]
629
+ }
630
+ };
631
+ } catch (error) {
632
+ console.error("Error in conversationHistoryProvider:", error);
633
+ return {
634
+ text: "Error retrieving conversation history"
635
+ };
636
+ }
637
+ }
638
+ };
639
+ var conversationHistory_default = conversationHistoryProvider;
640
+
641
+ // src/providers/callState.ts
642
+ var callStateProvider = {
643
+ name: "twilioCallState",
644
+ description: "Provides information about active voice calls and streams",
645
+ get: async (runtime, message, state) => {
646
+ try {
647
+ const twilioService = runtime.getService(
648
+ TWILIO_SERVICE_NAME
649
+ );
650
+ if (!twilioService) {
651
+ return {
652
+ text: "No Twilio call state available - service not initialized"
653
+ };
654
+ }
655
+ const activeStreams = twilioService.voiceStreams;
656
+ if (!activeStreams || activeStreams.size === 0) {
657
+ return {
658
+ text: "No active voice calls",
659
+ data: {
660
+ activeCallCount: 0
661
+ }
662
+ };
663
+ }
664
+ const callInfo = [];
665
+ const callData = [];
666
+ activeStreams.forEach((stream, callSid) => {
667
+ callInfo.push(`Call ${callSid}: ${stream.from} \u2192 ${stream.to}`);
668
+ callData.push({
669
+ callSid,
670
+ streamSid: stream.streamSid,
671
+ from: stream.from,
672
+ to: stream.to
673
+ });
674
+ });
675
+ return {
676
+ text: `Active voice calls (${activeStreams.size}):
677
+ ${callInfo.join("\n")}`,
678
+ data: {
679
+ activeCallCount: activeStreams.size,
680
+ calls: callData
681
+ }
682
+ };
683
+ } catch (error) {
684
+ console.error("Error in callStateProvider:", error);
685
+ return {
686
+ text: "Error retrieving call state"
687
+ };
688
+ }
689
+ }
690
+ };
691
+ var callState_default = callStateProvider;
692
+
693
+ // src/service.ts
694
+ import {
695
+ ChannelType,
696
+ ContentType,
697
+ EventType,
698
+ Service,
699
+ createMessageMemory,
700
+ createUniqueUuid,
701
+ logger as logger5,
702
+ stringToUuid
703
+ } from "@elizaos/core";
704
+ import bodyParser from "body-parser";
705
+ import express from "express";
706
+ import NodeCache from "node-cache";
707
+ import twilio from "twilio";
708
+ import { WebSocketServer } from "ws";
709
+
710
+ // src/types.ts
711
+ import { z } from "zod";
712
+ var E164_REGEX = /^\+\d{1,15}$/;
713
+ var MESSAGING_REGEX = /^(whatsapp:)?\+\d{1,15}$/;
714
+ var TwilioMessageSchema = z.object({
715
+ to: z.string().regex(MESSAGING_REGEX),
716
+ body: z.string().min(1).max(1600),
717
+ mediaUrl: z.array(z.string().url()).optional()
718
+ });
719
+ var TwilioCallSchema = z.object({
720
+ to: z.string().regex(E164_REGEX),
721
+ twiml: z.string().optional(),
722
+ url: z.string().url().optional()
723
+ });
724
+ var SendSmsSchema = z.object({
725
+ to: z.string().regex(MESSAGING_REGEX),
726
+ message: z.string().min(1).max(1600),
727
+ mediaUrl: z.array(z.string().url()).optional()
728
+ });
729
+ var MakeCallSchema = z.object({
730
+ to: z.string().regex(E164_REGEX),
731
+ message: z.string().optional(),
732
+ url: z.string().url().optional()
733
+ });
734
+ var SendMmsSchema = z.object({
735
+ to: z.string().regex(MESSAGING_REGEX),
736
+ message: z.string().min(1).max(1600),
737
+ mediaUrl: z.array(z.string().url()).min(1)
738
+ });
739
+ var CACHE_KEYS = {
740
+ CONVERSATION: (phoneNumber) => `twilio:conversation:${phoneNumber}`,
741
+ CALL_STATE: (callSid) => `twilio:call:${callSid}`,
742
+ MEDIA: (mediaSid) => `twilio:media:${mediaSid}`
743
+ };
744
+ var TwilioError = class extends Error {
745
+ constructor(message, code, twilioCode) {
746
+ super(message);
747
+ this.code = code;
748
+ this.twilioCode = twilioCode;
749
+ this.name = "TwilioError";
750
+ }
751
+ };
752
+
753
+ // src/service.ts
754
+ var getMessageService = (runtime) => {
755
+ if ("messageService" in runtime) {
756
+ const withMessageService = runtime;
757
+ return withMessageService.messageService ?? null;
758
+ }
759
+ return null;
760
+ };
761
+ var TwilioService = class _TwilioService extends Service {
762
+ static serviceType = TWILIO_SERVICE_NAME;
763
+ // Required static methods for Service type
764
+ static async start(runtime) {
765
+ const service = new _TwilioService();
766
+ await service.initialize(runtime);
767
+ return service;
768
+ }
769
+ static async stop(_runtime) {
770
+ return;
771
+ }
772
+ twilioConfig;
773
+ client;
774
+ // Twilio client
775
+ app;
776
+ server;
777
+ wss;
778
+ cache;
779
+ voiceStreams;
780
+ isInitialized = false;
781
+ constructor() {
782
+ super();
783
+ this.voiceStreams = /* @__PURE__ */ new Map();
784
+ this.cache = new NodeCache({ stdTTL: 600 });
785
+ }
786
+ async initialize(runtime) {
787
+ if (this.isInitialized) {
788
+ logger5.warn("TwilioService already initialized");
789
+ return;
790
+ }
791
+ this.runtime = runtime;
792
+ const configuredPhoneNumber = runtime.getSetting(
793
+ "TWILIO_PHONE_NUMBER"
794
+ );
795
+ const normalizedPhoneNumber = stripWhatsAppPrefix(
796
+ configuredPhoneNumber || ""
797
+ );
798
+ this.twilioConfig = {
799
+ accountSid: runtime.getSetting("TWILIO_ACCOUNT_SID"),
800
+ authToken: runtime.getSetting("TWILIO_AUTH_TOKEN"),
801
+ phoneNumber: normalizedPhoneNumber,
802
+ webhookUrl: runtime.getSetting("TWILIO_WEBHOOK_URL"),
803
+ webhookPort: parseInt(
804
+ runtime.getSetting("TWILIO_WEBHOOK_PORT") || "3000"
805
+ )
806
+ };
807
+ if (!this.twilioConfig.accountSid || !this.twilioConfig.authToken || !this.twilioConfig.phoneNumber) {
808
+ throw new TwilioError(ERROR_MESSAGES.MISSING_CREDENTIALS);
809
+ }
810
+ this.client = twilio(
811
+ this.twilioConfig.accountSid,
812
+ this.twilioConfig.authToken
813
+ );
814
+ await this.setupWebhookServer();
815
+ await this.updatePhoneNumberWebhooks();
816
+ this.isInitialized = true;
817
+ logger5.info("TwilioService initialized successfully");
818
+ }
819
+ // Implement stop method required by Service interface
820
+ async stop() {
821
+ await this.cleanup();
822
+ }
823
+ // Add capability description getter
824
+ get capabilityDescription() {
825
+ return "Twilio voice and SMS integration service for bidirectional communication";
826
+ }
827
+ async setupWebhookServer() {
828
+ this.app = express();
829
+ this.app.use(bodyParser.urlencoded({ extended: false }));
830
+ this.app.use(bodyParser.json());
831
+ this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.SMS, async (req, res) => {
832
+ try {
833
+ const webhook = req.body;
834
+ await this.handleIncomingSms(webhook);
835
+ res.type("text/xml").send("<Response></Response>");
836
+ } catch (error) {
837
+ logger5.error({ error: String(error) }, "Error handling SMS webhook");
838
+ res.status(500).send("<Response></Response>");
839
+ }
840
+ });
841
+ this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE, async (req, res) => {
842
+ try {
843
+ const webhook = req.body;
844
+ const twiml = await this.handleIncomingCall(webhook);
845
+ res.type("text/xml").send(twiml);
846
+ } catch (error) {
847
+ logger5.error({ error: String(error) }, "Error handling voice webhook");
848
+ res.type("text/xml").send(TWILIO_CONSTANTS.TWIML.DEFAULT_VOICE_RESPONSE);
849
+ }
850
+ });
851
+ this.app.post(TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS, (req, res) => {
852
+ const webhook = req.body;
853
+ const {
854
+ MessageSid,
855
+ MessageStatus,
856
+ SmsStatus,
857
+ CallSid,
858
+ CallStatus,
859
+ ErrorCode,
860
+ ErrorMessage,
861
+ To,
862
+ From,
863
+ MessagingServiceSid,
864
+ AccountSid,
865
+ ApiVersion
866
+ } = webhook;
867
+ logger5.info(
868
+ {
869
+ MessageSid,
870
+ MessageStatus,
871
+ SmsStatus,
872
+ CallSid,
873
+ CallStatus,
874
+ ErrorCode,
875
+ ErrorMessage,
876
+ To,
877
+ From,
878
+ MessagingServiceSid,
879
+ AccountSid,
880
+ ApiVersion
881
+ },
882
+ "Status callback received"
883
+ );
884
+ res.sendStatus(200);
885
+ });
886
+ this.server = this.app.listen(this.twilioConfig.webhookPort, () => {
887
+ logger5.info(
888
+ `Twilio webhook server listening on port ${this.twilioConfig.webhookPort}`
889
+ );
890
+ });
891
+ this.wss = new WebSocketServer({ server: this.server });
892
+ this.setupVoiceStreamingWebSocket();
893
+ }
894
+ setupVoiceStreamingWebSocket() {
895
+ this.wss.on("connection", (ws, req) => {
896
+ const url = new URL(req.url, `http://${req.headers.host}`);
897
+ const callSid = url.searchParams.get("callSid");
898
+ if (!callSid) {
899
+ logger5.error("No callSid provided for voice stream");
900
+ ws.close();
901
+ return;
902
+ }
903
+ logger5.info(`Voice stream connected for call ${callSid}`);
904
+ ws.on("message", async (data) => {
905
+ try {
906
+ const message = JSON.parse(data.toString());
907
+ switch (message.event) {
908
+ case "start":
909
+ this.handleStreamStart(callSid, message, ws);
910
+ break;
911
+ case "media":
912
+ this.handleStreamMedia(callSid, message);
913
+ break;
914
+ case "stop":
915
+ this.handleStreamStop(callSid);
916
+ break;
917
+ }
918
+ } catch (error) {
919
+ logger5.error(
920
+ { error: String(error) },
921
+ "Error processing voice stream message"
922
+ );
923
+ }
924
+ });
925
+ ws.on("close", () => {
926
+ logger5.info(`Voice stream closed for call ${callSid}`);
927
+ this.voiceStreams.delete(callSid);
928
+ if (this.runtime) {
929
+ this.runtime.emitEvent("voice:stream:ended" /* VOICE_STREAM_ENDED */, {
930
+ callSid
931
+ });
932
+ }
933
+ });
934
+ });
935
+ }
936
+ handleStreamStart(callSid, message, ws) {
937
+ const stream = {
938
+ streamSid: message.streamSid,
939
+ callSid,
940
+ from: message.start.customParameters.from,
941
+ to: message.start.customParameters.to,
942
+ socket: ws
943
+ };
944
+ this.voiceStreams.set(callSid, stream);
945
+ if (this.runtime) {
946
+ this.runtime.emitEvent("voice:stream:started" /* VOICE_STREAM_STARTED */, { stream });
947
+ }
948
+ }
949
+ handleStreamMedia(callSid, message) {
950
+ const stream = this.voiceStreams.get(callSid);
951
+ if (!stream) return;
952
+ const audioBuffer = Buffer.from(message.media.payload, "base64");
953
+ if (this.runtime) {
954
+ this.runtime.emitEvent("audio:received", {
955
+ callSid,
956
+ audio: audioBuffer,
957
+ timestamp: message.media.timestamp
958
+ });
959
+ }
960
+ }
961
+ handleStreamStop(callSid) {
962
+ this.voiceStreams.delete(callSid);
963
+ if (this.runtime) {
964
+ this.runtime.emitEvent("voice:stream:ended" /* VOICE_STREAM_ENDED */, { callSid });
965
+ }
966
+ }
967
+ async updatePhoneNumberWebhooks() {
968
+ try {
969
+ const phoneNumbers = await this.client.incomingPhoneNumbers.list({
970
+ phoneNumber: this.twilioConfig.phoneNumber
971
+ });
972
+ if (phoneNumbers.length === 0) {
973
+ throw new TwilioError(
974
+ `Phone number ${this.twilioConfig.phoneNumber} not found`
975
+ );
976
+ }
977
+ const phoneNumber = phoneNumbers[0];
978
+ await this.client.incomingPhoneNumbers(phoneNumber.sid).update({
979
+ smsUrl: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.SMS}`,
980
+ smsMethod: "POST",
981
+ voiceUrl: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE}`,
982
+ voiceMethod: "POST",
983
+ statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`,
984
+ statusCallbackMethod: "POST"
985
+ });
986
+ logger5.info(
987
+ `Updated webhooks for phone number ${this.twilioConfig.phoneNumber}`
988
+ );
989
+ } catch (error) {
990
+ logger5.error(
991
+ { error: String(error) },
992
+ "Error updating phone number webhooks"
993
+ );
994
+ throw error;
995
+ }
996
+ }
997
+ async sendSms(to, body, mediaUrl, fromOverride) {
998
+ const normalizedTo = formatMessagingAddress(to);
999
+ if (!normalizedTo || !validateMessagingAddress(normalizedTo)) {
1000
+ throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
1001
+ }
1002
+ const normalizedFrom = formatMessagingAddress(
1003
+ fromOverride || this.twilioConfig.phoneNumber
1004
+ );
1005
+ if (!normalizedFrom) {
1006
+ throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
1007
+ }
1008
+ const fromNumber = isWhatsAppAddress(normalizedTo) ? `whatsapp:${stripWhatsAppPrefix(normalizedFrom)}` : stripWhatsAppPrefix(normalizedFrom);
1009
+ try {
1010
+ const message = await this.client.messages.create({
1011
+ from: fromNumber,
1012
+ to: normalizedTo,
1013
+ body,
1014
+ mediaUrl,
1015
+ statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`
1016
+ });
1017
+ const twilioMessage = {
1018
+ sid: message.sid,
1019
+ from: message.from,
1020
+ to: message.to,
1021
+ body: message.body,
1022
+ direction: "outbound",
1023
+ status: message.status,
1024
+ dateCreated: message.dateCreated,
1025
+ dateSent: message.dateSent || void 0
1026
+ };
1027
+ const conversationKey = CACHE_KEYS.CONVERSATION(normalizedTo);
1028
+ let conversationHistory = this.cache.get(conversationKey) || [];
1029
+ conversationHistory.push(twilioMessage);
1030
+ if (conversationHistory.length > 50) {
1031
+ conversationHistory = conversationHistory.slice(-50);
1032
+ }
1033
+ this.cache.set(
1034
+ conversationKey,
1035
+ conversationHistory,
1036
+ TWILIO_CONSTANTS.CACHE_TTL.CONVERSATION
1037
+ );
1038
+ if (this.runtime) {
1039
+ this.runtime.emitEvent("sms:sent" /* SMS_SENT */, twilioMessage);
1040
+ }
1041
+ return twilioMessage;
1042
+ } catch (error) {
1043
+ throw new TwilioError(
1044
+ `Failed to send SMS: ${error.message}`,
1045
+ error.code,
1046
+ error.moreInfo
1047
+ );
1048
+ }
1049
+ }
1050
+ async makeCall(to, twiml, url) {
1051
+ if (!validatePhoneNumber(to)) {
1052
+ throw new TwilioError(ERROR_MESSAGES.INVALID_PHONE_NUMBER);
1053
+ }
1054
+ try {
1055
+ const callParams = {
1056
+ from: this.twilioConfig.phoneNumber,
1057
+ to,
1058
+ statusCallback: `${this.twilioConfig.webhookUrl}${TWILIO_CONSTANTS.WEBHOOK_PATHS.STATUS}`,
1059
+ statusCallbackEvent: ["initiated", "ringing", "answered", "completed"]
1060
+ };
1061
+ if (twiml) {
1062
+ callParams.twiml = twiml;
1063
+ } else if (url) {
1064
+ callParams.url = url;
1065
+ } else {
1066
+ callParams.twiml = TWILIO_CONSTANTS.TWIML.DEFAULT_VOICE_RESPONSE;
1067
+ }
1068
+ const call = await this.client.calls.create(callParams);
1069
+ const twilioCall = {
1070
+ sid: call.sid,
1071
+ from: call.from,
1072
+ to: call.to,
1073
+ status: call.status,
1074
+ direction: "outbound",
1075
+ dateCreated: call.dateCreated
1076
+ };
1077
+ this.cache.set(
1078
+ CACHE_KEYS.CALL_STATE(call.sid),
1079
+ twilioCall,
1080
+ TWILIO_CONSTANTS.CACHE_TTL.CALL_STATE
1081
+ );
1082
+ return twilioCall;
1083
+ } catch (error) {
1084
+ throw new TwilioError(
1085
+ `Failed to make call: ${error.message}`,
1086
+ error.code,
1087
+ error.moreInfo
1088
+ );
1089
+ }
1090
+ }
1091
+ async handleIncomingSms(webhook) {
1092
+ const message = {
1093
+ sid: webhook.MessageSid,
1094
+ from: webhook.From,
1095
+ to: webhook.To,
1096
+ body: webhook.Body,
1097
+ direction: "inbound",
1098
+ status: "received",
1099
+ dateCreated: /* @__PURE__ */ new Date()
1100
+ };
1101
+ if (webhook.NumMedia && parseInt(webhook.NumMedia) > 0) {
1102
+ message.media = [];
1103
+ for (let i = 0; i < parseInt(webhook.NumMedia); i++) {
1104
+ const mediaUrl = webhook[`MediaUrl${i}`];
1105
+ const contentType = webhook[`MediaContentType${i}`];
1106
+ if (mediaUrl) {
1107
+ message.media.push({
1108
+ url: mediaUrl,
1109
+ contentType: contentType || "unknown",
1110
+ sid: `media_${i}`
1111
+ });
1112
+ }
1113
+ }
1114
+ }
1115
+ const conversationKey = CACHE_KEYS.CONVERSATION(webhook.From);
1116
+ let conversationHistory = this.cache.get(conversationKey) || [];
1117
+ conversationHistory.push(message);
1118
+ if (conversationHistory.length > 50) {
1119
+ conversationHistory = conversationHistory.slice(-50);
1120
+ }
1121
+ this.cache.set(
1122
+ conversationKey,
1123
+ conversationHistory,
1124
+ TWILIO_CONSTANTS.CACHE_TTL.CONVERSATION
1125
+ );
1126
+ if (this.runtime) {
1127
+ this.runtime.emitEvent("sms:received" /* SMS_RECEIVED */, message);
1128
+ }
1129
+ await this.processIncomingMessage(message);
1130
+ }
1131
+ async handleIncomingCall(webhook) {
1132
+ const call = {
1133
+ sid: webhook.CallSid,
1134
+ from: webhook.From,
1135
+ to: webhook.To,
1136
+ status: webhook.CallStatus,
1137
+ direction: "inbound",
1138
+ dateCreated: /* @__PURE__ */ new Date()
1139
+ };
1140
+ this.cache.set(
1141
+ CACHE_KEYS.CALL_STATE(webhook.CallSid),
1142
+ call,
1143
+ TWILIO_CONSTANTS.CACHE_TTL.CALL_STATE
1144
+ );
1145
+ if (this.runtime) {
1146
+ this.runtime.emitEvent("call:received" /* CALL_RECEIVED */, call);
1147
+ }
1148
+ const streamUrl = `wss://${new URL(this.twilioConfig.webhookUrl).host}${TWILIO_CONSTANTS.WEBHOOK_PATHS.VOICE_STREAM}?callSid=${webhook.CallSid}`;
1149
+ return TWILIO_CONSTANTS.TWIML.STREAM_RESPONSE(streamUrl);
1150
+ }
1151
+ async startVoiceStream(callSid) {
1152
+ const stream = this.voiceStreams.get(callSid);
1153
+ if (!stream) {
1154
+ throw new TwilioError(`No voice stream found for call ${callSid}`);
1155
+ }
1156
+ logger5.info(`Starting voice stream processing for call ${callSid}`);
1157
+ }
1158
+ async endVoiceStream(callSid) {
1159
+ const stream = this.voiceStreams.get(callSid);
1160
+ if (stream && stream.socket) {
1161
+ stream.socket.close();
1162
+ }
1163
+ this.voiceStreams.delete(callSid);
1164
+ }
1165
+ async processIncomingMessage(message) {
1166
+ try {
1167
+ const text = message.body?.trim();
1168
+ if (!text) {
1169
+ return;
1170
+ }
1171
+ const source = isWhatsAppAddress(message.from) ? "whatsapp" : "twilio";
1172
+ const entityId = createUniqueUuid(this.runtime, message.from);
1173
+ const roomId = createUniqueUuid(this.runtime, `twilio:${message.from}`);
1174
+ const worldId = createUniqueUuid(this.runtime, `twilio:${message.to}`);
1175
+ await this.runtime.ensureConnection({
1176
+ entityId,
1177
+ roomId,
1178
+ worldId,
1179
+ userName: message.from,
1180
+ source,
1181
+ channelId: message.from,
1182
+ type: ChannelType.DM
1183
+ });
1184
+ const attachments = this.buildMediaAttachments(message.media);
1185
+ const memory = createMessageMemory({
1186
+ id: stringToUuid(message.sid),
1187
+ entityId,
1188
+ roomId,
1189
+ content: {
1190
+ text,
1191
+ source,
1192
+ channelType: ChannelType.DM,
1193
+ phoneNumber: message.from,
1194
+ attachments: attachments.length > 0 ? attachments : void 0
1195
+ }
1196
+ });
1197
+ const callback = async (content) => {
1198
+ const responseText = typeof content.text === "string" ? content.text.trim() : "";
1199
+ if (!responseText) {
1200
+ return [];
1201
+ }
1202
+ await this.sendSms(message.from, responseText, void 0, message.to);
1203
+ return [];
1204
+ };
1205
+ const messageService = getMessageService(this.runtime);
1206
+ if (messageService) {
1207
+ await messageService.handleMessage(this.runtime, memory, callback);
1208
+ } else {
1209
+ logger5.warn("messageService unavailable; falling back to event emit");
1210
+ await this.runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
1211
+ message: memory,
1212
+ callback,
1213
+ source
1214
+ });
1215
+ }
1216
+ } catch (error) {
1217
+ logger5.error(
1218
+ { error: String(error) },
1219
+ "Error processing incoming message"
1220
+ );
1221
+ }
1222
+ }
1223
+ buildMediaAttachments(media) {
1224
+ if (!media || media.length === 0) {
1225
+ return [];
1226
+ }
1227
+ return media.map((item) => ({
1228
+ id: item.sid,
1229
+ url: item.url,
1230
+ contentType: this.resolveContentType(item.contentType)
1231
+ }));
1232
+ }
1233
+ resolveContentType(contentType) {
1234
+ if (contentType.startsWith("image/")) {
1235
+ return ContentType.IMAGE;
1236
+ }
1237
+ if (contentType.startsWith("video/")) {
1238
+ return ContentType.VIDEO;
1239
+ }
1240
+ if (contentType.startsWith("audio/")) {
1241
+ return ContentType.AUDIO;
1242
+ }
1243
+ return ContentType.DOCUMENT;
1244
+ }
1245
+ sendStreamAudio(callSid, audio) {
1246
+ const stream = this.voiceStreams.get(callSid);
1247
+ if (!stream || !stream.socket) return;
1248
+ const payload = {
1249
+ event: "media",
1250
+ streamSid: stream.streamSid,
1251
+ media: {
1252
+ payload: audio.toString("base64")
1253
+ }
1254
+ };
1255
+ stream.socket.send(JSON.stringify(payload));
1256
+ }
1257
+ async cleanup() {
1258
+ for (const [callSid, stream] of this.voiceStreams) {
1259
+ if (stream.socket) {
1260
+ stream.socket.close();
1261
+ }
1262
+ }
1263
+ this.voiceStreams.clear();
1264
+ if (this.wss) {
1265
+ this.wss.close();
1266
+ }
1267
+ if (this.server) {
1268
+ this.server.close();
1269
+ }
1270
+ this.cache.flushAll();
1271
+ this.isInitialized = false;
1272
+ logger5.info("TwilioService cleaned up");
1273
+ }
1274
+ // Getters for service information
1275
+ get serviceType() {
1276
+ return TWILIO_SERVICE_NAME;
1277
+ }
1278
+ get serviceName() {
1279
+ return "twilio";
1280
+ }
1281
+ get phoneNumber() {
1282
+ return this.twilioConfig.phoneNumber;
1283
+ }
1284
+ get isConnected() {
1285
+ return this.isInitialized;
1286
+ }
1287
+ // Add public method to get conversation history
1288
+ getConversationHistory(phoneNumber, limit = 10) {
1289
+ const cacheKey = CACHE_KEYS.CONVERSATION(phoneNumber);
1290
+ const messages = this.cache.get(cacheKey);
1291
+ if (!messages || !Array.isArray(messages)) {
1292
+ return [];
1293
+ }
1294
+ return messages.slice(-limit);
1295
+ }
1296
+ // Add method to get call state
1297
+ getCallState(callSid) {
1298
+ const cacheKey = CACHE_KEYS.CALL_STATE(callSid);
1299
+ return this.cache.get(cacheKey);
1300
+ }
1301
+ };
1302
+
1303
+ // src/tests.ts
1304
+ import {
1305
+ logger as logger6
1306
+ } from "@elizaos/core";
1307
+ import * as readline from "readline";
1308
+ import axios from "axios";
1309
+ var TwilioTestSuite = class {
1310
+ name = "Twilio Plugin Test Suite";
1311
+ description = "Tests for Twilio voice and SMS functionality";
1312
+ tests = [
1313
+ {
1314
+ name: "Service Initialization Test",
1315
+ fn: async (runtime) => {
1316
+ const twilioService = runtime.getService(
1317
+ TWILIO_SERVICE_NAME
1318
+ );
1319
+ if (!twilioService) {
1320
+ throw new Error("Twilio service not initialized");
1321
+ }
1322
+ if (!twilioService.isConnected) {
1323
+ throw new Error("Twilio service is not connected");
1324
+ }
1325
+ if (!twilioService.phoneNumber) {
1326
+ throw new Error("Twilio phone number not configured");
1327
+ }
1328
+ logger6.info(
1329
+ `\u2705 Service initialized with phone number: ${twilioService.phoneNumber}`
1330
+ );
1331
+ }
1332
+ },
1333
+ {
1334
+ name: "Send SMS Test",
1335
+ fn: async (runtime) => {
1336
+ const twilioService = runtime.getService(
1337
+ TWILIO_SERVICE_NAME
1338
+ );
1339
+ if (!twilioService) {
1340
+ throw new Error("Twilio service not initialized");
1341
+ }
1342
+ const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
1343
+ if (!testNumber) {
1344
+ logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping SMS test");
1345
+ return;
1346
+ }
1347
+ const result = await twilioService.sendSms(
1348
+ testNumber,
1349
+ "Test SMS from Eliza Twilio plugin"
1350
+ );
1351
+ logger6.info(`\u2705 SMS test successful. Message SID: ${result.sid}`);
1352
+ }
1353
+ },
1354
+ {
1355
+ name: "Send MMS Test",
1356
+ fn: async (runtime) => {
1357
+ const twilioService = runtime.getService(
1358
+ TWILIO_SERVICE_NAME
1359
+ );
1360
+ if (!twilioService) {
1361
+ throw new Error("Twilio service not initialized");
1362
+ }
1363
+ const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
1364
+ if (!testNumber) {
1365
+ logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping MMS test");
1366
+ return;
1367
+ }
1368
+ const result = await twilioService.sendSms(
1369
+ testNumber,
1370
+ "Test MMS from Eliza Twilio plugin",
1371
+ ["https://demo.twilio.com/owl.png"]
1372
+ // Twilio's demo image
1373
+ );
1374
+ logger6.info(`\u2705 MMS test successful. Message SID: ${result.sid}`);
1375
+ }
1376
+ },
1377
+ {
1378
+ name: "Make Call Test",
1379
+ fn: async (runtime) => {
1380
+ const twilioService = runtime.getService(
1381
+ TWILIO_SERVICE_NAME
1382
+ );
1383
+ if (!twilioService) {
1384
+ throw new Error("Twilio service not initialized");
1385
+ }
1386
+ const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
1387
+ if (!testNumber) {
1388
+ logger6.warn("TWILIO_TEST_PHONE_NUMBER not set, skipping call test");
1389
+ return;
1390
+ }
1391
+ const twiml = `<?xml version="1.0" encoding="UTF-8"?>
1392
+ <Response>
1393
+ <Say voice="alice">This is a test call from Eliza Twilio plugin. Goodbye!</Say>
1394
+ <Hangup/>
1395
+ </Response>`;
1396
+ const result = await twilioService.makeCall(testNumber, twiml);
1397
+ logger6.info(`\u2705 Call test successful. Call SID: ${result.sid}`);
1398
+ }
1399
+ },
1400
+ {
1401
+ name: "Webhook Server Test",
1402
+ fn: async (runtime) => {
1403
+ const twilioService = runtime.getService(
1404
+ TWILIO_SERVICE_NAME
1405
+ );
1406
+ if (!twilioService) {
1407
+ throw new Error("Twilio service not initialized");
1408
+ }
1409
+ if (!twilioService.isConnected) {
1410
+ throw new Error("Webhook server is not running");
1411
+ }
1412
+ const webhookPort = runtime.getSetting("TWILIO_WEBHOOK_PORT") || "3000";
1413
+ logger6.info(`\u2705 Webhook server is running on port ${webhookPort}`);
1414
+ const webhookUrl = runtime.getSetting("TWILIO_WEBHOOK_URL");
1415
+ if (webhookUrl && webhookUrl.includes("localhost")) {
1416
+ try {
1417
+ const smsResponse = await axios.post(
1418
+ `http://localhost:${webhookPort}/webhooks/twilio/sms`,
1419
+ {
1420
+ MessageSid: "TEST123",
1421
+ From: "+18885551234",
1422
+ To: twilioService.phoneNumber,
1423
+ Body: "Test webhook message"
1424
+ },
1425
+ {
1426
+ headers: {
1427
+ "Content-Type": "application/x-www-form-urlencoded"
1428
+ }
1429
+ }
1430
+ );
1431
+ if (smsResponse.status === 200) {
1432
+ logger6.info("\u2705 SMS webhook endpoint is responding");
1433
+ }
1434
+ const voiceResponse = await axios.post(
1435
+ `http://localhost:${webhookPort}/webhooks/twilio/voice`,
1436
+ {
1437
+ CallSid: "CATEST123",
1438
+ From: "+18885551234",
1439
+ To: twilioService.phoneNumber,
1440
+ CallStatus: "ringing"
1441
+ },
1442
+ {
1443
+ headers: {
1444
+ "Content-Type": "application/x-www-form-urlencoded"
1445
+ }
1446
+ }
1447
+ );
1448
+ if (voiceResponse.status === 200) {
1449
+ logger6.info("\u2705 Voice webhook endpoint is responding");
1450
+ }
1451
+ } catch (error) {
1452
+ logger6.warn(
1453
+ { error: String(error) },
1454
+ "Could not test webhook endpoints locally"
1455
+ );
1456
+ }
1457
+ }
1458
+ }
1459
+ },
1460
+ {
1461
+ name: "Conversation History Test",
1462
+ fn: async (runtime) => {
1463
+ const twilioService = runtime.getService(
1464
+ TWILIO_SERVICE_NAME
1465
+ );
1466
+ if (!twilioService) {
1467
+ throw new Error("Twilio service not initialized");
1468
+ }
1469
+ const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
1470
+ if (!testNumber) {
1471
+ logger6.warn(
1472
+ "TWILIO_TEST_PHONE_NUMBER not set, skipping conversation history test"
1473
+ );
1474
+ return;
1475
+ }
1476
+ await twilioService.sendSms(testNumber, "History test message");
1477
+ const history = twilioService.getConversationHistory(testNumber, 5);
1478
+ if (history.length > 0) {
1479
+ logger6.info(
1480
+ `\u2705 Conversation history retrieved: ${history.length} messages`
1481
+ );
1482
+ logger6.info(` Latest message: ${history[history.length - 1].body}`);
1483
+ } else {
1484
+ logger6.info(
1485
+ "\u2705 Conversation history is empty (expected for new number)"
1486
+ );
1487
+ }
1488
+ }
1489
+ },
1490
+ {
1491
+ name: "Error Handling Test",
1492
+ fn: async (runtime) => {
1493
+ const twilioService = runtime.getService(
1494
+ TWILIO_SERVICE_NAME
1495
+ );
1496
+ if (!twilioService) {
1497
+ throw new Error("Twilio service not initialized");
1498
+ }
1499
+ try {
1500
+ await twilioService.sendSms("invalid-number", "Test");
1501
+ throw new Error("Expected error for invalid phone number");
1502
+ } catch (error) {
1503
+ if (error.message.includes("Invalid phone number")) {
1504
+ logger6.info("\u2705 Invalid phone number error handled correctly");
1505
+ } else {
1506
+ throw error;
1507
+ }
1508
+ }
1509
+ try {
1510
+ await twilioService.sendSms("+18885551234", "");
1511
+ logger6.info("\u2705 Empty message handled");
1512
+ } catch (error) {
1513
+ logger6.info("\u2705 Empty message error handled correctly");
1514
+ }
1515
+ }
1516
+ },
1517
+ {
1518
+ name: "Interactive Test Mode",
1519
+ fn: async (runtime) => {
1520
+ const twilioService = runtime.getService(
1521
+ TWILIO_SERVICE_NAME
1522
+ );
1523
+ if (!twilioService) {
1524
+ throw new Error("Twilio service not initialized");
1525
+ }
1526
+ const phoneNumber = runtime.getSetting("TWILIO_PHONE_NUMBER");
1527
+ const testNumber = runtime.getSetting("TWILIO_TEST_PHONE_NUMBER");
1528
+ if (!phoneNumber || !testNumber) {
1529
+ throw new Error(
1530
+ "TWILIO_PHONE_NUMBER and TWILIO_TEST_PHONE_NUMBER must be set for interactive testing"
1531
+ );
1532
+ }
1533
+ logger6.info("\n\u{1F3AE} INTERACTIVE TWILIO TEST MODE");
1534
+ logger6.info("================================");
1535
+ logger6.info(`\u{1F4F1} Your Twilio Number: ${phoneNumber}`);
1536
+ logger6.info(`\u{1F4F1} Test Target Number: ${testNumber}`);
1537
+ logger6.info("\n\u{1F4CB} Instructions:");
1538
+ logger6.info(
1539
+ "1. The webhook server is running and listening for incoming messages"
1540
+ );
1541
+ logger6.info(
1542
+ "2. Text or call your Twilio number to test incoming messages"
1543
+ );
1544
+ logger6.info("3. The test will send a test SMS and make a test call");
1545
+ logger6.info("4. Watch the console for incoming message logs");
1546
+ logger6.info("\nPress Enter to start the interactive test...");
1547
+ await new Promise((resolve) => {
1548
+ const rl = readline.createInterface({
1549
+ input: process.stdin,
1550
+ output: process.stdout
1551
+ });
1552
+ rl.question("", () => {
1553
+ rl.close();
1554
+ resolve();
1555
+ });
1556
+ });
1557
+ logger6.info("\n\u{1F4E4} Test 1: Sending SMS...");
1558
+ try {
1559
+ const smsResult = await twilioService.sendSms(
1560
+ testNumber,
1561
+ "\u{1F389} Interactive test SMS from ElizaOS! Reply to test two-way messaging."
1562
+ );
1563
+ logger6.info(`\u2705 SMS sent! SID: ${smsResult.sid}`);
1564
+ logger6.info(` Status: ${smsResult.status}`);
1565
+ } catch (error) {
1566
+ logger6.error(`\u274C SMS failed: ${error}`);
1567
+ }
1568
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
1569
+ logger6.info("\n\u{1F4E4} Test 2: Making call...");
1570
+ try {
1571
+ const twiml = `<?xml version="1.0" encoding="UTF-8"?>
1572
+ <Response>
1573
+ <Say voice="alice">Hello from ElizaOS interactive test! This call confirms your Twilio integration is working.</Say>
1574
+ <Play>https://api.twilio.com/cowbell.mp3</Play>
1575
+ <Say voice="alice">Thank you for testing. Goodbye!</Say>
1576
+ </Response>`;
1577
+ const callResult = await twilioService.makeCall(testNumber, twiml);
1578
+ logger6.info(`\u2705 Call initiated! SID: ${callResult.sid}`);
1579
+ logger6.info(` Status: ${callResult.status}`);
1580
+ } catch (error) {
1581
+ logger6.error(`\u274C Call failed: ${error}`);
1582
+ }
1583
+ logger6.info("\n\u{1F4E5} Test 3: Waiting for incoming messages...");
1584
+ logger6.info(" Text your Twilio number now!");
1585
+ logger6.info(" The server will log any incoming SMS");
1586
+ logger6.info("\n\u23F1\uFE0F Test will continue for 30 seconds...");
1587
+ await new Promise((resolve) => setTimeout(resolve, 3e4));
1588
+ logger6.info("\n\u2728 Interactive test complete!");
1589
+ logger6.info("Check logs for detailed results.");
1590
+ }
1591
+ }
1592
+ ];
1593
+ };
1594
+
1595
+ // src/index.ts
1596
+ var twilioPlugin = {
1597
+ name: "twilio",
1598
+ description: "Twilio plugin for bidirectional voice and text messaging integration",
1599
+ services: [TwilioService],
1600
+ actions: [sendSms_default, makeCall_default, sendMms_default],
1601
+ providers: [conversationHistory_default, callState_default],
1602
+ tests: [new TwilioTestSuite()],
1603
+ init: async (config, runtime) => {
1604
+ const accountSid = runtime.getSetting("TWILIO_ACCOUNT_SID");
1605
+ const authToken = runtime.getSetting("TWILIO_AUTH_TOKEN");
1606
+ const phoneNumber = runtime.getSetting("TWILIO_PHONE_NUMBER");
1607
+ const webhookUrl = runtime.getSetting("TWILIO_WEBHOOK_URL");
1608
+ if (!accountSid || accountSid.trim() === "") {
1609
+ logger7.warn(
1610
+ "Twilio Account SID not provided - Twilio plugin is loaded but will not be functional"
1611
+ );
1612
+ logger7.warn(
1613
+ "To enable Twilio functionality, please provide TWILIO_ACCOUNT_SID in your .env file"
1614
+ );
1615
+ return;
1616
+ }
1617
+ if (!authToken || authToken.trim() === "") {
1618
+ logger7.warn(
1619
+ "Twilio Auth Token not provided - Twilio plugin is loaded but will not be functional"
1620
+ );
1621
+ logger7.warn(
1622
+ "To enable Twilio functionality, please provide TWILIO_AUTH_TOKEN in your .env file"
1623
+ );
1624
+ return;
1625
+ }
1626
+ if (!phoneNumber || phoneNumber.trim() === "") {
1627
+ logger7.warn(
1628
+ "Twilio Phone Number not provided - Twilio plugin is loaded but will not be functional"
1629
+ );
1630
+ logger7.warn(
1631
+ "To enable Twilio functionality, please provide TWILIO_PHONE_NUMBER in your .env file"
1632
+ );
1633
+ return;
1634
+ }
1635
+ if (!webhookUrl || webhookUrl.trim() === "") {
1636
+ logger7.warn(
1637
+ "Twilio Webhook URL not provided - Twilio will not be able to receive incoming messages or calls"
1638
+ );
1639
+ logger7.warn(
1640
+ "To enable incoming communication, please provide TWILIO_WEBHOOK_URL in your .env file"
1641
+ );
1642
+ }
1643
+ logger7.info("Twilio plugin initialized successfully");
1644
+ }
1645
+ };
1646
+ var index_default = twilioPlugin;
1647
+ export {
1648
+ index_default as default
1649
+ };
1650
+ //# sourceMappingURL=index.js.map