@openvole/paw-voice-call 0.1.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/README.md +76 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +379 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/vole-paw.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @openvole/paw-voice-call
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@openvole/paw-voice-call)
|
|
4
|
+
|
|
5
|
+
Voice call channel Paw for OpenVole. Enables voice conversations via Twilio — both inbound and outbound calls with real-time speech-to-text and text-to-speech.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @openvole/paw-voice-call
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Twilio Account Setup
|
|
14
|
+
|
|
15
|
+
1. Create a [Twilio account](https://www.twilio.com/try-twilio)
|
|
16
|
+
2. Purchase a phone number with Voice capabilities
|
|
17
|
+
3. Note your Account SID and Auth Token from the Twilio Console
|
|
18
|
+
4. Configure your phone number's Voice webhook to point to your server (see Webhook Setup below)
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Add the paw to your `vole.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"paws": [
|
|
27
|
+
{
|
|
28
|
+
"name": "@openvole/paw-voice-call",
|
|
29
|
+
"config": {}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Environment Variables
|
|
36
|
+
|
|
37
|
+
| Variable | Required | Description |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `TWILIO_ACCOUNT_SID` | Yes | Your Twilio Account SID |
|
|
40
|
+
| `TWILIO_AUTH_TOKEN` | Yes | Your Twilio Auth Token |
|
|
41
|
+
| `TWILIO_PHONE_NUMBER` | Yes | Your Twilio phone number in E.164 format (e.g. `+14155551234`) |
|
|
42
|
+
| `VOICE_CALL_WEBHOOK_URL` | Yes | Public URL where Twilio can reach the webhook server |
|
|
43
|
+
| `VOICE_CALL_PORT` | No | Local port for the webhook server (default: `3979`) |
|
|
44
|
+
|
|
45
|
+
## Call Flow
|
|
46
|
+
|
|
47
|
+
1. **Inbound call** arrives at Twilio, which hits `POST /voice/inbound`
|
|
48
|
+
2. Server responds with TwiML greeting + `<Gather>` to collect speech
|
|
49
|
+
3. Caller speaks — Twilio's built-in STT transcribes and sends to `POST /voice/gather`
|
|
50
|
+
4. Paw creates a task with the transcription, responds with TwiML redirect to `/voice/respond/<taskId>`
|
|
51
|
+
5. The respond endpoint holds the connection (long-poll, max 25s) waiting for the brain's response
|
|
52
|
+
6. When the task completes, TwiML with `<Say>` delivers the response + `<Gather>` continues the conversation
|
|
53
|
+
7. Loop back to step 3
|
|
54
|
+
|
|
55
|
+
Outbound calls follow the same flow after initial connection via the `initiate_call` tool.
|
|
56
|
+
|
|
57
|
+
## Webhook Setup
|
|
58
|
+
|
|
59
|
+
### Development (ngrok)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
ngrok http 3979
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Set `VOICE_CALL_WEBHOOK_URL` to your ngrok URL (e.g. `https://abc123.ngrok-free.app`).
|
|
66
|
+
|
|
67
|
+
In your Twilio Console, configure your phone number's Voice webhook:
|
|
68
|
+
- **A call comes in**: Webhook, `https://abc123.ngrok-free.app/voice/inbound`, HTTP POST
|
|
69
|
+
|
|
70
|
+
### Production
|
|
71
|
+
|
|
72
|
+
Point `VOICE_CALL_WEBHOOK_URL` to your production server's public URL and configure the Twilio webhook accordingly.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { definePaw } from "@openvole/paw-sdk";
|
|
3
|
+
|
|
4
|
+
// src/paw.ts
|
|
5
|
+
import { z } from "@openvole/paw-sdk";
|
|
6
|
+
import { createIpcTransport } from "@openvole/paw-sdk";
|
|
7
|
+
|
|
8
|
+
// src/twilio.ts
|
|
9
|
+
import http from "http";
|
|
10
|
+
import { URL } from "url";
|
|
11
|
+
import Twilio from "twilio";
|
|
12
|
+
var TwilioClient = class {
|
|
13
|
+
twilioClient;
|
|
14
|
+
fromNumber;
|
|
15
|
+
webhookUrl;
|
|
16
|
+
port;
|
|
17
|
+
server;
|
|
18
|
+
speechCallback;
|
|
19
|
+
activeCalls = /* @__PURE__ */ new Map();
|
|
20
|
+
pendingResponses = /* @__PURE__ */ new Map();
|
|
21
|
+
constructor(accountSid, authToken, fromNumber, webhookUrl, port = 3979) {
|
|
22
|
+
this.twilioClient = Twilio(accountSid, authToken);
|
|
23
|
+
this.fromNumber = fromNumber;
|
|
24
|
+
this.webhookUrl = webhookUrl.replace(/\/$/, "");
|
|
25
|
+
this.port = port;
|
|
26
|
+
}
|
|
27
|
+
onSpeech(callback) {
|
|
28
|
+
this.speechCallback = callback;
|
|
29
|
+
}
|
|
30
|
+
async initiateCall(to, greeting) {
|
|
31
|
+
const greetingText = greeting || "Hello, this is OpenVole. How can I help you?";
|
|
32
|
+
const twiml = [
|
|
33
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
34
|
+
"<Response>",
|
|
35
|
+
` <Say>${escapeXml(greetingText)}</Say>`,
|
|
36
|
+
` <Gather input="speech" speechTimeout="auto" action="${this.webhookUrl}/voice/gather">`,
|
|
37
|
+
" <Say>I'm listening.</Say>",
|
|
38
|
+
" </Gather>",
|
|
39
|
+
"</Response>"
|
|
40
|
+
].join("\n");
|
|
41
|
+
const call = await this.twilioClient.calls.create({
|
|
42
|
+
to,
|
|
43
|
+
from: this.fromNumber,
|
|
44
|
+
twiml,
|
|
45
|
+
statusCallback: `${this.webhookUrl}/voice/status`,
|
|
46
|
+
statusCallbackEvent: ["initiated", "ringing", "answered", "completed"]
|
|
47
|
+
});
|
|
48
|
+
this.activeCalls.set(call.sid, {
|
|
49
|
+
callSid: call.sid,
|
|
50
|
+
from: this.fromNumber,
|
|
51
|
+
to,
|
|
52
|
+
status: "initiated"
|
|
53
|
+
});
|
|
54
|
+
console.log(`[paw-voice-call] Initiated call ${call.sid} to ${to}`);
|
|
55
|
+
return { callSid: call.sid };
|
|
56
|
+
}
|
|
57
|
+
async endCall(callSid) {
|
|
58
|
+
await this.twilioClient.calls(callSid).update({ status: "completed" });
|
|
59
|
+
this.activeCalls.delete(callSid);
|
|
60
|
+
console.log(`[paw-voice-call] Ended call ${callSid}`);
|
|
61
|
+
}
|
|
62
|
+
respondToCall(taskId, responseText) {
|
|
63
|
+
const pending = this.pendingResponses.get(taskId);
|
|
64
|
+
if (!pending) {
|
|
65
|
+
console.warn(`[paw-voice-call] No pending response for taskId ${taskId}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
clearTimeout(pending.timer);
|
|
69
|
+
this.pendingResponses.delete(taskId);
|
|
70
|
+
const twiml = [
|
|
71
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
72
|
+
"<Response>",
|
|
73
|
+
` <Say>${escapeXml(responseText)}</Say>`,
|
|
74
|
+
` <Gather input="speech" speechTimeout="auto" action="${this.webhookUrl}/voice/gather">`,
|
|
75
|
+
" <Say>Is there anything else?</Say>",
|
|
76
|
+
" </Gather>",
|
|
77
|
+
"</Response>"
|
|
78
|
+
].join("\n");
|
|
79
|
+
pending.resolve(twiml);
|
|
80
|
+
}
|
|
81
|
+
async start() {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
this.server = http.createServer((req, res) => {
|
|
84
|
+
this.handleRequest(req, res);
|
|
85
|
+
});
|
|
86
|
+
this.server.listen(this.port, () => {
|
|
87
|
+
console.log(`[paw-voice-call] Webhook server listening on port ${this.port}`);
|
|
88
|
+
resolve();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async stop() {
|
|
93
|
+
for (const [taskId, pending] of this.pendingResponses) {
|
|
94
|
+
clearTimeout(pending.timer);
|
|
95
|
+
pending.resolve(
|
|
96
|
+
'<?xml version="1.0" encoding="UTF-8"?><Response><Say>Goodbye.</Say><Hangup/></Response>'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
this.pendingResponses.clear();
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
if (this.server) {
|
|
102
|
+
this.server.close(() => {
|
|
103
|
+
console.log("[paw-voice-call] Webhook server stopped");
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
resolve();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
handleRequest(req, res) {
|
|
112
|
+
const url = new URL(req.url || "/", `http://localhost:${this.port}`);
|
|
113
|
+
const path = url.pathname;
|
|
114
|
+
if (req.method !== "POST") {
|
|
115
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
116
|
+
res.end("Method Not Allowed");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let body = "";
|
|
120
|
+
req.on("data", (chunk) => {
|
|
121
|
+
body += chunk.toString();
|
|
122
|
+
});
|
|
123
|
+
req.on("end", () => {
|
|
124
|
+
const params = parseFormData(body);
|
|
125
|
+
if (path === "/voice/inbound") {
|
|
126
|
+
this.handleInbound(params, res);
|
|
127
|
+
} else if (path === "/voice/gather") {
|
|
128
|
+
this.handleGather(params, res);
|
|
129
|
+
} else if (path.startsWith("/voice/respond/")) {
|
|
130
|
+
const taskId = path.replace("/voice/respond/", "");
|
|
131
|
+
this.handleRespond(taskId, res);
|
|
132
|
+
} else if (path === "/voice/status") {
|
|
133
|
+
this.handleStatus(params, res);
|
|
134
|
+
} else {
|
|
135
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
136
|
+
res.end("Not Found");
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
handleInbound(params, res) {
|
|
141
|
+
const callSid = params.CallSid || "";
|
|
142
|
+
const from = params.From || "";
|
|
143
|
+
const to = params.To || "";
|
|
144
|
+
console.log(`[paw-voice-call] Inbound call ${callSid} from ${from}`);
|
|
145
|
+
this.activeCalls.set(callSid, {
|
|
146
|
+
callSid,
|
|
147
|
+
from,
|
|
148
|
+
to,
|
|
149
|
+
status: "in-progress"
|
|
150
|
+
});
|
|
151
|
+
const twiml = [
|
|
152
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
153
|
+
"<Response>",
|
|
154
|
+
" <Say>Hello, welcome to OpenVole.</Say>",
|
|
155
|
+
` <Gather input="speech" speechTimeout="auto" action="${this.webhookUrl}/voice/gather">`,
|
|
156
|
+
" <Say>How can I help you?</Say>",
|
|
157
|
+
" </Gather>",
|
|
158
|
+
"</Response>"
|
|
159
|
+
].join("\n");
|
|
160
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
161
|
+
res.end(twiml);
|
|
162
|
+
}
|
|
163
|
+
handleGather(params, res) {
|
|
164
|
+
const speechResult = params.SpeechResult || "";
|
|
165
|
+
const callSid = params.CallSid || "";
|
|
166
|
+
const from = params.From || "";
|
|
167
|
+
if (!speechResult) {
|
|
168
|
+
const twiml2 = [
|
|
169
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
170
|
+
"<Response>",
|
|
171
|
+
` <Gather input="speech" speechTimeout="auto" action="${this.webhookUrl}/voice/gather">`,
|
|
172
|
+
" <Say>I didn't catch that. Could you please repeat?</Say>",
|
|
173
|
+
" </Gather>",
|
|
174
|
+
"</Response>"
|
|
175
|
+
].join("\n");
|
|
176
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
177
|
+
res.end(twiml2);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(`[paw-voice-call] Speech from ${from} on call ${callSid}: ${speechResult}`);
|
|
181
|
+
if (this.speechCallback) {
|
|
182
|
+
this.speechCallback(speechResult, callSid, from);
|
|
183
|
+
}
|
|
184
|
+
const taskId = this.lastTaskId;
|
|
185
|
+
this.lastTaskId = void 0;
|
|
186
|
+
if (!taskId) {
|
|
187
|
+
const twiml2 = [
|
|
188
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
189
|
+
"<Response>",
|
|
190
|
+
" <Say>Sorry, I could not process your request. Please try again.</Say>",
|
|
191
|
+
` <Gather input="speech" speechTimeout="auto" action="${this.webhookUrl}/voice/gather">`,
|
|
192
|
+
" <Say>How can I help you?</Say>",
|
|
193
|
+
" </Gather>",
|
|
194
|
+
"</Response>"
|
|
195
|
+
].join("\n");
|
|
196
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
197
|
+
res.end(twiml2);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const twiml = [
|
|
201
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
202
|
+
"<Response>",
|
|
203
|
+
" <Say>One moment please.</Say>",
|
|
204
|
+
` <Redirect method="POST">${this.webhookUrl}/voice/respond/${taskId}</Redirect>`,
|
|
205
|
+
"</Response>"
|
|
206
|
+
].join("\n");
|
|
207
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
208
|
+
res.end(twiml);
|
|
209
|
+
}
|
|
210
|
+
handleRespond(taskId, res) {
|
|
211
|
+
console.log(`[paw-voice-call] Long-poll waiting for task ${taskId}`);
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
this.pendingResponses.delete(taskId);
|
|
214
|
+
const twiml = [
|
|
215
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
216
|
+
"<Response>",
|
|
217
|
+
" <Say>I'm still working on that. Please hold on.</Say>",
|
|
218
|
+
` <Redirect method="POST">${this.webhookUrl}/voice/respond/${taskId}</Redirect>`,
|
|
219
|
+
"</Response>"
|
|
220
|
+
].join("\n");
|
|
221
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
222
|
+
res.end(twiml);
|
|
223
|
+
}, 25e3);
|
|
224
|
+
this.pendingResponses.set(taskId, {
|
|
225
|
+
resolve: (twiml) => {
|
|
226
|
+
res.writeHead(200, { "Content-Type": "application/xml" });
|
|
227
|
+
res.end(twiml);
|
|
228
|
+
},
|
|
229
|
+
timer
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
handleStatus(params, res) {
|
|
233
|
+
const callSid = params.CallSid || "";
|
|
234
|
+
const callStatus = params.CallStatus || "";
|
|
235
|
+
console.log(`[paw-voice-call] Call ${callSid} status: ${callStatus}`);
|
|
236
|
+
const call = this.activeCalls.get(callSid);
|
|
237
|
+
if (call) {
|
|
238
|
+
call.status = callStatus;
|
|
239
|
+
if (callStatus === "completed" || callStatus === "failed" || callStatus === "canceled" || callStatus === "busy" || callStatus === "no-answer") {
|
|
240
|
+
this.activeCalls.delete(callSid);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
244
|
+
res.end("OK");
|
|
245
|
+
}
|
|
246
|
+
/** Used by the paw to pass the taskId back synchronously after creating a task */
|
|
247
|
+
lastTaskId;
|
|
248
|
+
setLastTaskId(taskId) {
|
|
249
|
+
this.lastTaskId = taskId;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
function parseFormData(body) {
|
|
253
|
+
const params = {};
|
|
254
|
+
for (const pair of body.split("&")) {
|
|
255
|
+
const [key, ...rest] = pair.split("=");
|
|
256
|
+
if (key) {
|
|
257
|
+
params[decodeURIComponent(key)] = decodeURIComponent(rest.join("=").replace(/\+/g, " "));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return params;
|
|
261
|
+
}
|
|
262
|
+
function escapeXml(str) {
|
|
263
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/paw.ts
|
|
267
|
+
var client;
|
|
268
|
+
var transport;
|
|
269
|
+
var pendingTasks = /* @__PURE__ */ new Map();
|
|
270
|
+
var paw = {
|
|
271
|
+
name: "@openvole/paw-voice-call",
|
|
272
|
+
version: "0.1.0",
|
|
273
|
+
description: "Voice call channel for OpenVole via Twilio",
|
|
274
|
+
tools: [
|
|
275
|
+
{
|
|
276
|
+
name: "initiate_call",
|
|
277
|
+
description: "Initiate an outbound phone call",
|
|
278
|
+
parameters: z.object({
|
|
279
|
+
to: z.string().describe("Phone number in E.164 format (e.g. +14155551234)"),
|
|
280
|
+
greeting: z.string().optional().describe("Optional greeting message to say when the call connects")
|
|
281
|
+
}),
|
|
282
|
+
async execute(params) {
|
|
283
|
+
const { to, greeting } = params;
|
|
284
|
+
if (!client) throw new Error("Twilio client not initialized");
|
|
285
|
+
const result = await client.initiateCall(to, greeting);
|
|
286
|
+
return { ok: true, call_sid: result.callSid };
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "end_call",
|
|
291
|
+
description: "End an active phone call",
|
|
292
|
+
parameters: z.object({
|
|
293
|
+
call_sid: z.string().describe("The Twilio Call SID of the call to end")
|
|
294
|
+
}),
|
|
295
|
+
async execute(params) {
|
|
296
|
+
const { call_sid } = params;
|
|
297
|
+
if (!client) throw new Error("Twilio client not initialized");
|
|
298
|
+
await client.endCall(call_sid);
|
|
299
|
+
return { ok: true };
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: "list_calls",
|
|
304
|
+
description: "List active phone calls",
|
|
305
|
+
parameters: z.object({}),
|
|
306
|
+
async execute() {
|
|
307
|
+
if (!client) throw new Error("Twilio client not initialized");
|
|
308
|
+
const calls = Array.from(client.activeCalls.values());
|
|
309
|
+
return { ok: true, calls };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
],
|
|
313
|
+
async onLoad() {
|
|
314
|
+
const accountSid = process.env.TWILIO_ACCOUNT_SID;
|
|
315
|
+
const authToken = process.env.TWILIO_AUTH_TOKEN;
|
|
316
|
+
const fromNumber = process.env.TWILIO_PHONE_NUMBER;
|
|
317
|
+
const webhookUrl = process.env.VOICE_CALL_WEBHOOK_URL;
|
|
318
|
+
const port = process.env.VOICE_CALL_PORT ? Number.parseInt(process.env.VOICE_CALL_PORT, 10) : 3979;
|
|
319
|
+
if (!accountSid || !authToken || !fromNumber || !webhookUrl) {
|
|
320
|
+
console.error(
|
|
321
|
+
"[paw-voice-call] Missing required env vars: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, VOICE_CALL_WEBHOOK_URL"
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
transport = createIpcTransport();
|
|
326
|
+
transport.subscribe(["task:completed", "task:failed"]);
|
|
327
|
+
transport.onBusEvent((event, data) => {
|
|
328
|
+
const taskData = data;
|
|
329
|
+
const taskId = taskData?.taskId;
|
|
330
|
+
if (!taskId) return;
|
|
331
|
+
const origin = pendingTasks.get(taskId);
|
|
332
|
+
if (!origin) return;
|
|
333
|
+
pendingTasks.delete(taskId);
|
|
334
|
+
if (event === "task:completed" && taskData.result) {
|
|
335
|
+
client?.respondToCall(taskId, taskData.result);
|
|
336
|
+
} else if (event === "task:failed") {
|
|
337
|
+
const errorMsg = taskData.error || "Sorry, something went wrong processing your request.";
|
|
338
|
+
client?.respondToCall(taskId, errorMsg);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
try {
|
|
342
|
+
client = new TwilioClient(accountSid, authToken, fromNumber, webhookUrl, port);
|
|
343
|
+
client.onSpeech(async (text, callSid, from) => {
|
|
344
|
+
console.log(`[paw-voice-call] Speech from ${from} on call ${callSid}: ${text}`);
|
|
345
|
+
try {
|
|
346
|
+
const { taskId } = await transport.createTask(text, {
|
|
347
|
+
sessionId: `call:${callSid}`,
|
|
348
|
+
source: "voice-call",
|
|
349
|
+
callSid,
|
|
350
|
+
from
|
|
351
|
+
});
|
|
352
|
+
pendingTasks.set(taskId, { callSid, taskId });
|
|
353
|
+
client.setLastTaskId(taskId);
|
|
354
|
+
} catch (err) {
|
|
355
|
+
console.error("[paw-voice-call] Failed to create task:", err);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
await client.start();
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.error("[paw-voice-call] Failed to start Twilio client:", err);
|
|
361
|
+
client = void 0;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
async onUnload() {
|
|
365
|
+
if (client) {
|
|
366
|
+
await client.stop();
|
|
367
|
+
client = void 0;
|
|
368
|
+
}
|
|
369
|
+
transport = void 0;
|
|
370
|
+
pendingTasks.clear();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// src/index.ts
|
|
375
|
+
var index_default = definePaw(paw);
|
|
376
|
+
export {
|
|
377
|
+
index_default as default
|
|
378
|
+
};
|
|
379
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/paw.ts","../src/twilio.ts"],"sourcesContent":["import { definePaw } from '@openvole/paw-sdk'\nimport { paw } from './paw.js'\n\nexport default definePaw(paw)\n","import { z, type PawDefinition } from '@openvole/paw-sdk'\nimport { createIpcTransport } from '@openvole/paw-sdk'\nimport { TwilioClient } from './twilio.js'\n\nlet client: TwilioClient | undefined\nlet transport: ReturnType<typeof createIpcTransport> | undefined\n\n/** Map from taskId to the originating call context */\nconst pendingTasks = new Map<string, { callSid: string; taskId: string }>()\n\nexport const paw: PawDefinition = {\n\tname: '@openvole/paw-voice-call',\n\tversion: '0.1.0',\n\tdescription: 'Voice call channel for OpenVole via Twilio',\n\n\ttools: [\n\t\t{\n\t\t\tname: 'initiate_call',\n\t\t\tdescription: 'Initiate an outbound phone call',\n\t\t\tparameters: z.object({\n\t\t\t\tto: z.string().describe('Phone number in E.164 format (e.g. +14155551234)'),\n\t\t\t\tgreeting: z.string().optional().describe('Optional greeting message to say when the call connects'),\n\t\t\t}),\n\t\t\tasync execute(params) {\n\t\t\t\tconst { to, greeting } = params as { to: string; greeting?: string }\n\t\t\t\tif (!client) throw new Error('Twilio client not initialized')\n\t\t\t\tconst result = await client.initiateCall(to, greeting)\n\t\t\t\treturn { ok: true, call_sid: result.callSid }\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: 'end_call',\n\t\t\tdescription: 'End an active phone call',\n\t\t\tparameters: z.object({\n\t\t\t\tcall_sid: z.string().describe('The Twilio Call SID of the call to end'),\n\t\t\t}),\n\t\t\tasync execute(params) {\n\t\t\t\tconst { call_sid } = params as { call_sid: string }\n\t\t\t\tif (!client) throw new Error('Twilio client not initialized')\n\t\t\t\tawait client.endCall(call_sid)\n\t\t\t\treturn { ok: true }\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: 'list_calls',\n\t\t\tdescription: 'List active phone calls',\n\t\t\tparameters: z.object({}),\n\t\t\tasync execute() {\n\t\t\t\tif (!client) throw new Error('Twilio client not initialized')\n\t\t\t\tconst calls = Array.from(client.activeCalls.values())\n\t\t\t\treturn { ok: true, calls }\n\t\t\t},\n\t\t},\n\t],\n\n\tasync onLoad() {\n\t\tconst accountSid = process.env.TWILIO_ACCOUNT_SID\n\t\tconst authToken = process.env.TWILIO_AUTH_TOKEN\n\t\tconst fromNumber = process.env.TWILIO_PHONE_NUMBER\n\t\tconst webhookUrl = process.env.VOICE_CALL_WEBHOOK_URL\n\t\tconst port = process.env.VOICE_CALL_PORT ? Number.parseInt(process.env.VOICE_CALL_PORT, 10) : 3979\n\n\t\tif (!accountSid || !authToken || !fromNumber || !webhookUrl) {\n\t\t\tconsole.error(\n\t\t\t\t'[paw-voice-call] Missing required env vars: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, VOICE_CALL_WEBHOOK_URL',\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\ttransport = createIpcTransport()\n\n\t\t// Subscribe to task lifecycle events so we can send voice responses back\n\t\ttransport.subscribe(['task:completed', 'task:failed'])\n\n\t\ttransport.onBusEvent((event, data) => {\n\t\t\tconst taskData = data as {\n\t\t\t\ttaskId?: string\n\t\t\t\tresult?: string\n\t\t\t\terror?: string\n\t\t\t}\n\t\t\tconst taskId = taskData?.taskId\n\t\t\tif (!taskId) return\n\n\t\t\tconst origin = pendingTasks.get(taskId)\n\t\t\tif (!origin) return\n\n\t\t\tpendingTasks.delete(taskId)\n\n\t\t\tif (event === 'task:completed' && taskData.result) {\n\t\t\t\tclient?.respondToCall(taskId, taskData.result)\n\t\t\t} else if (event === 'task:failed') {\n\t\t\t\tconst errorMsg = taskData.error || 'Sorry, something went wrong processing your request.'\n\t\t\t\tclient?.respondToCall(taskId, errorMsg)\n\t\t\t}\n\t\t})\n\n\t\ttry {\n\t\t\tclient = new TwilioClient(accountSid, authToken, fromNumber, webhookUrl, port)\n\n\t\t\tclient.onSpeech(async (text, callSid, from) => {\n\t\t\t\tconsole.log(`[paw-voice-call] Speech from ${from} on call ${callSid}: ${text}`)\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { taskId } = await transport!.createTask(text, {\n\t\t\t\t\t\tsessionId: `call:${callSid}`,\n\t\t\t\t\t\tsource: 'voice-call',\n\t\t\t\t\t\tcallSid,\n\t\t\t\t\t\tfrom,\n\t\t\t\t\t})\n\n\t\t\t\t\tpendingTasks.set(taskId, { callSid, taskId })\n\t\t\t\t\tclient!.setLastTaskId(taskId)\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error('[paw-voice-call] Failed to create task:', err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tawait client.start()\n\t\t} catch (err) {\n\t\t\tconsole.error('[paw-voice-call] Failed to start Twilio client:', err)\n\t\t\tclient = undefined\n\t\t}\n\t},\n\n\tasync onUnload() {\n\t\tif (client) {\n\t\t\tawait client.stop()\n\t\t\tclient = undefined\n\t\t}\n\t\ttransport = undefined\n\t\tpendingTasks.clear()\n\t},\n}\n","import http from 'node:http'\nimport { URL } from 'node:url'\nimport Twilio from 'twilio'\n\ntype SpeechCallback = (text: string, callSid: string, from: string) => void\n\ninterface ActiveCall {\n\tcallSid: string\n\tfrom: string\n\tto: string\n\tstatus: string\n}\n\ninterface PendingResponse {\n\tresolve: (twiml: string) => void\n\ttimer: ReturnType<typeof setTimeout>\n}\n\nexport class TwilioClient {\n\tprivate readonly twilioClient: ReturnType<typeof Twilio>\n\tprivate readonly fromNumber: string\n\tprivate readonly webhookUrl: string\n\tprivate readonly port: number\n\tprivate server: http.Server | undefined\n\tprivate speechCallback: SpeechCallback | undefined\n\n\treadonly activeCalls = new Map<string, ActiveCall>()\n\tprivate readonly pendingResponses = new Map<string, PendingResponse>()\n\n\tconstructor(\n\t\taccountSid: string,\n\t\tauthToken: string,\n\t\tfromNumber: string,\n\t\twebhookUrl: string,\n\t\tport = 3979,\n\t) {\n\t\tthis.twilioClient = Twilio(accountSid, authToken)\n\t\tthis.fromNumber = fromNumber\n\t\tthis.webhookUrl = webhookUrl.replace(/\\/$/, '')\n\t\tthis.port = port\n\t}\n\n\tonSpeech(callback: SpeechCallback): void {\n\t\tthis.speechCallback = callback\n\t}\n\n\tasync initiateCall(to: string, greeting?: string): Promise<{ callSid: string }> {\n\t\tconst greetingText = greeting || 'Hello, this is OpenVole. How can I help you?'\n\t\tconst twiml = [\n\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t'<Response>',\n\t\t\t` <Say>${escapeXml(greetingText)}</Say>`,\n\t\t\t` <Gather input=\"speech\" speechTimeout=\"auto\" action=\"${this.webhookUrl}/voice/gather\">`,\n\t\t\t' <Say>I\\'m listening.</Say>',\n\t\t\t' </Gather>',\n\t\t\t'</Response>',\n\t\t].join('\\n')\n\n\t\tconst call = await this.twilioClient.calls.create({\n\t\t\tto,\n\t\t\tfrom: this.fromNumber,\n\t\t\ttwiml,\n\t\t\tstatusCallback: `${this.webhookUrl}/voice/status`,\n\t\t\tstatusCallbackEvent: ['initiated', 'ringing', 'answered', 'completed'],\n\t\t})\n\n\t\tthis.activeCalls.set(call.sid, {\n\t\t\tcallSid: call.sid,\n\t\t\tfrom: this.fromNumber,\n\t\t\tto,\n\t\t\tstatus: 'initiated',\n\t\t})\n\n\t\tconsole.log(`[paw-voice-call] Initiated call ${call.sid} to ${to}`)\n\t\treturn { callSid: call.sid }\n\t}\n\n\tasync endCall(callSid: string): Promise<void> {\n\t\tawait this.twilioClient.calls(callSid).update({ status: 'completed' })\n\t\tthis.activeCalls.delete(callSid)\n\t\tconsole.log(`[paw-voice-call] Ended call ${callSid}`)\n\t}\n\n\trespondToCall(taskId: string, responseText: string): void {\n\t\tconst pending = this.pendingResponses.get(taskId)\n\t\tif (!pending) {\n\t\t\tconsole.warn(`[paw-voice-call] No pending response for taskId ${taskId}`)\n\t\t\treturn\n\t\t}\n\n\t\tclearTimeout(pending.timer)\n\t\tthis.pendingResponses.delete(taskId)\n\n\t\tconst twiml = [\n\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t'<Response>',\n\t\t\t` <Say>${escapeXml(responseText)}</Say>`,\n\t\t\t` <Gather input=\"speech\" speechTimeout=\"auto\" action=\"${this.webhookUrl}/voice/gather\">`,\n\t\t\t' <Say>Is there anything else?</Say>',\n\t\t\t' </Gather>',\n\t\t\t'</Response>',\n\t\t].join('\\n')\n\n\t\tpending.resolve(twiml)\n\t}\n\n\tasync start(): Promise<void> {\n\t\treturn new Promise((resolve) => {\n\t\t\tthis.server = http.createServer((req, res) => {\n\t\t\t\tthis.handleRequest(req, res)\n\t\t\t})\n\n\t\t\tthis.server.listen(this.port, () => {\n\t\t\t\tconsole.log(`[paw-voice-call] Webhook server listening on port ${this.port}`)\n\t\t\t\tresolve()\n\t\t\t})\n\t\t})\n\t}\n\n\tasync stop(): Promise<void> {\n\t\t// Clear any pending responses\n\t\tfor (const [taskId, pending] of this.pendingResponses) {\n\t\t\tclearTimeout(pending.timer)\n\t\t\tpending.resolve(\n\t\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Say>Goodbye.</Say><Hangup/></Response>',\n\t\t\t)\n\t\t}\n\t\tthis.pendingResponses.clear()\n\n\t\treturn new Promise((resolve) => {\n\t\t\tif (this.server) {\n\t\t\t\tthis.server.close(() => {\n\t\t\t\t\tconsole.log('[paw-voice-call] Webhook server stopped')\n\t\t\t\t\tresolve()\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tresolve()\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n\t\tconst url = new URL(req.url || '/', `http://localhost:${this.port}`)\n\t\tconst path = url.pathname\n\n\t\tif (req.method !== 'POST') {\n\t\t\tres.writeHead(405, { 'Content-Type': 'text/plain' })\n\t\t\tres.end('Method Not Allowed')\n\t\t\treturn\n\t\t}\n\n\t\tlet body = ''\n\t\treq.on('data', (chunk: Buffer) => {\n\t\t\tbody += chunk.toString()\n\t\t})\n\n\t\treq.on('end', () => {\n\t\t\tconst params = parseFormData(body)\n\n\t\t\tif (path === '/voice/inbound') {\n\t\t\t\tthis.handleInbound(params, res)\n\t\t\t} else if (path === '/voice/gather') {\n\t\t\t\tthis.handleGather(params, res)\n\t\t\t} else if (path.startsWith('/voice/respond/')) {\n\t\t\t\tconst taskId = path.replace('/voice/respond/', '')\n\t\t\t\tthis.handleRespond(taskId, res)\n\t\t\t} else if (path === '/voice/status') {\n\t\t\t\tthis.handleStatus(params, res)\n\t\t\t} else {\n\t\t\t\tres.writeHead(404, { 'Content-Type': 'text/plain' })\n\t\t\t\tres.end('Not Found')\n\t\t\t}\n\t\t})\n\t}\n\n\tprivate handleInbound(params: Record<string, string>, res: http.ServerResponse): void {\n\t\tconst callSid = params.CallSid || ''\n\t\tconst from = params.From || ''\n\t\tconst to = params.To || ''\n\n\t\tconsole.log(`[paw-voice-call] Inbound call ${callSid} from ${from}`)\n\n\t\tthis.activeCalls.set(callSid, {\n\t\t\tcallSid,\n\t\t\tfrom,\n\t\t\tto,\n\t\t\tstatus: 'in-progress',\n\t\t})\n\n\t\tconst twiml = [\n\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t'<Response>',\n\t\t\t' <Say>Hello, welcome to OpenVole.</Say>',\n\t\t\t` <Gather input=\"speech\" speechTimeout=\"auto\" action=\"${this.webhookUrl}/voice/gather\">`,\n\t\t\t' <Say>How can I help you?</Say>',\n\t\t\t' </Gather>',\n\t\t\t'</Response>',\n\t\t].join('\\n')\n\n\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\tres.end(twiml)\n\t}\n\n\tprivate handleGather(params: Record<string, string>, res: http.ServerResponse): void {\n\t\tconst speechResult = params.SpeechResult || ''\n\t\tconst callSid = params.CallSid || ''\n\t\tconst from = params.From || ''\n\n\t\tif (!speechResult) {\n\t\t\t// No speech detected, prompt again\n\t\t\tconst twiml = [\n\t\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t\t'<Response>',\n\t\t\t\t` <Gather input=\"speech\" speechTimeout=\"auto\" action=\"${this.webhookUrl}/voice/gather\">`,\n\t\t\t\t' <Say>I didn\\'t catch that. Could you please repeat?</Say>',\n\t\t\t\t' </Gather>',\n\t\t\t\t'</Response>',\n\t\t\t].join('\\n')\n\t\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\t\tres.end(twiml)\n\t\t\treturn\n\t\t}\n\n\t\tconsole.log(`[paw-voice-call] Speech from ${from} on call ${callSid}: ${speechResult}`)\n\n\t\t// Fire the speech callback — it will create a task and give us a taskId\n\t\tif (this.speechCallback) {\n\t\t\tthis.speechCallback(speechResult, callSid, from)\n\t\t}\n\n\t\t// Get the taskId that was just created (stored by the callback via setLastTaskId)\n\t\tconst taskId = this.lastTaskId\n\t\tthis.lastTaskId = undefined\n\n\t\tif (!taskId) {\n\t\t\t// Fallback if task creation failed\n\t\t\tconst twiml = [\n\t\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t\t'<Response>',\n\t\t\t\t' <Say>Sorry, I could not process your request. Please try again.</Say>',\n\t\t\t\t` <Gather input=\"speech\" speechTimeout=\"auto\" action=\"${this.webhookUrl}/voice/gather\">`,\n\t\t\t\t' <Say>How can I help you?</Say>',\n\t\t\t\t' </Gather>',\n\t\t\t\t'</Response>',\n\t\t\t].join('\\n')\n\t\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\t\tres.end(twiml)\n\t\t\treturn\n\t\t}\n\n\t\t// Respond with redirect to the long-poll endpoint\n\t\tconst twiml = [\n\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t'<Response>',\n\t\t\t' <Say>One moment please.</Say>',\n\t\t\t` <Redirect method=\"POST\">${this.webhookUrl}/voice/respond/${taskId}</Redirect>`,\n\t\t\t'</Response>',\n\t\t].join('\\n')\n\n\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\tres.end(twiml)\n\t}\n\n\tprivate handleRespond(taskId: string, res: http.ServerResponse): void {\n\t\tconsole.log(`[paw-voice-call] Long-poll waiting for task ${taskId}`)\n\n\t\tconst timer = setTimeout(() => {\n\t\t\t// Timeout — ask user to wait or retry\n\t\t\tthis.pendingResponses.delete(taskId)\n\t\t\tconst twiml = [\n\t\t\t\t'<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n\t\t\t\t'<Response>',\n\t\t\t\t' <Say>I\\'m still working on that. Please hold on.</Say>',\n\t\t\t\t` <Redirect method=\"POST\">${this.webhookUrl}/voice/respond/${taskId}</Redirect>`,\n\t\t\t\t'</Response>',\n\t\t\t].join('\\n')\n\t\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\t\tres.end(twiml)\n\t\t}, 25_000) // 25s to stay under Twilio's 30s limit\n\n\t\tthis.pendingResponses.set(taskId, {\n\t\t\tresolve: (twiml: string) => {\n\t\t\t\tres.writeHead(200, { 'Content-Type': 'application/xml' })\n\t\t\t\tres.end(twiml)\n\t\t\t},\n\t\t\ttimer,\n\t\t})\n\t}\n\n\tprivate handleStatus(params: Record<string, string>, res: http.ServerResponse): void {\n\t\tconst callSid = params.CallSid || ''\n\t\tconst callStatus = params.CallStatus || ''\n\n\t\tconsole.log(`[paw-voice-call] Call ${callSid} status: ${callStatus}`)\n\n\t\tconst call = this.activeCalls.get(callSid)\n\t\tif (call) {\n\t\t\tcall.status = callStatus\n\t\t\tif (callStatus === 'completed' || callStatus === 'failed' || callStatus === 'canceled' || callStatus === 'busy' || callStatus === 'no-answer') {\n\t\t\t\tthis.activeCalls.delete(callSid)\n\t\t\t}\n\t\t}\n\n\t\tres.writeHead(200, { 'Content-Type': 'text/plain' })\n\t\tres.end('OK')\n\t}\n\n\t/** Used by the paw to pass the taskId back synchronously after creating a task */\n\tprivate lastTaskId: string | undefined\n\n\tsetLastTaskId(taskId: string): void {\n\t\tthis.lastTaskId = taskId\n\t}\n}\n\n/** Parse URL-encoded form data */\nfunction parseFormData(body: string): Record<string, string> {\n\tconst params: Record<string, string> = {}\n\tfor (const pair of body.split('&')) {\n\t\tconst [key, ...rest] = pair.split('=')\n\t\tif (key) {\n\t\t\tparams[decodeURIComponent(key)] = decodeURIComponent(rest.join('=').replace(/\\+/g, ' '))\n\t\t}\n\t}\n\treturn params\n}\n\n/** Escape special XML characters */\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, '&')\n\t\t.replace(/</g, '<')\n\t\t.replace(/>/g, '>')\n\t\t.replace(/\"/g, '"')\n\t\t.replace(/'/g, ''')\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;;;ACA1B,SAAS,SAA6B;AACtC,SAAS,0BAA0B;;;ACDnC,OAAO,UAAU;AACjB,SAAS,WAAW;AACpB,OAAO,YAAY;AAgBZ,IAAM,eAAN,MAAmB;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EACA;AAAA,EAEC,cAAc,oBAAI,IAAwB;AAAA,EAClC,mBAAmB,oBAAI,IAA6B;AAAA,EAErE,YACC,YACA,WACA,YACA,YACA,OAAO,MACN;AACD,SAAK,eAAe,OAAO,YAAY,SAAS;AAChD,SAAK,aAAa;AAClB,SAAK,aAAa,WAAW,QAAQ,OAAO,EAAE;AAC9C,SAAK,OAAO;AAAA,EACb;AAAA,EAEA,SAAS,UAAgC;AACxC,SAAK,iBAAiB;AAAA,EACvB;AAAA,EAEA,MAAM,aAAa,IAAY,UAAiD;AAC/E,UAAM,eAAe,YAAY;AACjC,UAAM,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU,UAAU,YAAY,CAAC;AAAA,MACjC,yDAAyD,KAAK,UAAU;AAAA,MACxE;AAAA,MACA;AAAA,MACA;AAAA,IACD,EAAE,KAAK,IAAI;AAEX,UAAM,OAAO,MAAM,KAAK,aAAa,MAAM,OAAO;AAAA,MACjD;AAAA,MACA,MAAM,KAAK;AAAA,MACX;AAAA,MACA,gBAAgB,GAAG,KAAK,UAAU;AAAA,MAClC,qBAAqB,CAAC,aAAa,WAAW,YAAY,WAAW;AAAA,IACtE,CAAC;AAED,SAAK,YAAY,IAAI,KAAK,KAAK;AAAA,MAC9B,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACT,CAAC;AAED,YAAQ,IAAI,mCAAmC,KAAK,GAAG,OAAO,EAAE,EAAE;AAClE,WAAO,EAAE,SAAS,KAAK,IAAI;AAAA,EAC5B;AAAA,EAEA,MAAM,QAAQ,SAAgC;AAC7C,UAAM,KAAK,aAAa,MAAM,OAAO,EAAE,OAAO,EAAE,QAAQ,YAAY,CAAC;AACrE,SAAK,YAAY,OAAO,OAAO;AAC/B,YAAQ,IAAI,+BAA+B,OAAO,EAAE;AAAA,EACrD;AAAA,EAEA,cAAc,QAAgB,cAA4B;AACzD,UAAM,UAAU,KAAK,iBAAiB,IAAI,MAAM;AAChD,QAAI,CAAC,SAAS;AACb,cAAQ,KAAK,mDAAmD,MAAM,EAAE;AACxE;AAAA,IACD;AAEA,iBAAa,QAAQ,KAAK;AAC1B,SAAK,iBAAiB,OAAO,MAAM;AAEnC,UAAM,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,MACA,UAAU,UAAU,YAAY,CAAC;AAAA,MACjC,yDAAyD,KAAK,UAAU;AAAA,MACxE;AAAA,MACA;AAAA,MACA;AAAA,IACD,EAAE,KAAK,IAAI;AAEX,YAAQ,QAAQ,KAAK;AAAA,EACtB;AAAA,EAEA,MAAM,QAAuB;AAC5B,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC/B,WAAK,SAAS,KAAK,aAAa,CAAC,KAAK,QAAQ;AAC7C,aAAK,cAAc,KAAK,GAAG;AAAA,MAC5B,CAAC;AAED,WAAK,OAAO,OAAO,KAAK,MAAM,MAAM;AACnC,gBAAQ,IAAI,qDAAqD,KAAK,IAAI,EAAE;AAC5E,gBAAQ;AAAA,MACT,CAAC;AAAA,IACF,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,OAAsB;AAE3B,eAAW,CAAC,QAAQ,OAAO,KAAK,KAAK,kBAAkB;AACtD,mBAAa,QAAQ,KAAK;AAC1B,cAAQ;AAAA,QACP;AAAA,MACD;AAAA,IACD;AACA,SAAK,iBAAiB,MAAM;AAE5B,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC/B,UAAI,KAAK,QAAQ;AAChB,aAAK,OAAO,MAAM,MAAM;AACvB,kBAAQ,IAAI,yCAAyC;AACrD,kBAAQ;AAAA,QACT,CAAC;AAAA,MACF,OAAO;AACN,gBAAQ;AAAA,MACT;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,cAAc,KAA2B,KAAgC;AAChF,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,oBAAoB,KAAK,IAAI,EAAE;AACnE,UAAM,OAAO,IAAI;AAEjB,QAAI,IAAI,WAAW,QAAQ;AAC1B,UAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,UAAI,IAAI,oBAAoB;AAC5B;AAAA,IACD;AAEA,QAAI,OAAO;AACX,QAAI,GAAG,QAAQ,CAAC,UAAkB;AACjC,cAAQ,MAAM,SAAS;AAAA,IACxB,CAAC;AAED,QAAI,GAAG,OAAO,MAAM;AACnB,YAAM,SAAS,cAAc,IAAI;AAEjC,UAAI,SAAS,kBAAkB;AAC9B,aAAK,cAAc,QAAQ,GAAG;AAAA,MAC/B,WAAW,SAAS,iBAAiB;AACpC,aAAK,aAAa,QAAQ,GAAG;AAAA,MAC9B,WAAW,KAAK,WAAW,iBAAiB,GAAG;AAC9C,cAAM,SAAS,KAAK,QAAQ,mBAAmB,EAAE;AACjD,aAAK,cAAc,QAAQ,GAAG;AAAA,MAC/B,WAAW,SAAS,iBAAiB;AACpC,aAAK,aAAa,QAAQ,GAAG;AAAA,MAC9B,OAAO;AACN,YAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,YAAI,IAAI,WAAW;AAAA,MACpB;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,cAAc,QAAgC,KAAgC;AACrF,UAAM,UAAU,OAAO,WAAW;AAClC,UAAM,OAAO,OAAO,QAAQ;AAC5B,UAAM,KAAK,OAAO,MAAM;AAExB,YAAQ,IAAI,iCAAiC,OAAO,SAAS,IAAI,EAAE;AAEnE,SAAK,YAAY,IAAI,SAAS;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACT,CAAC;AAED,UAAM,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA,yDAAyD,KAAK,UAAU;AAAA,MACxE;AAAA,MACA;AAAA,MACA;AAAA,IACD,EAAE,KAAK,IAAI;AAEX,QAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,QAAI,IAAI,KAAK;AAAA,EACd;AAAA,EAEQ,aAAa,QAAgC,KAAgC;AACpF,UAAM,eAAe,OAAO,gBAAgB;AAC5C,UAAM,UAAU,OAAO,WAAW;AAClC,UAAM,OAAO,OAAO,QAAQ;AAE5B,QAAI,CAAC,cAAc;AAElB,YAAMA,SAAQ;AAAA,QACb;AAAA,QACA;AAAA,QACA,yDAAyD,KAAK,UAAU;AAAA,QACxE;AAAA,QACA;AAAA,QACA;AAAA,MACD,EAAE,KAAK,IAAI;AACX,UAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,UAAI,IAAIA,MAAK;AACb;AAAA,IACD;AAEA,YAAQ,IAAI,gCAAgC,IAAI,YAAY,OAAO,KAAK,YAAY,EAAE;AAGtF,QAAI,KAAK,gBAAgB;AACxB,WAAK,eAAe,cAAc,SAAS,IAAI;AAAA,IAChD;AAGA,UAAM,SAAS,KAAK;AACpB,SAAK,aAAa;AAElB,QAAI,CAAC,QAAQ;AAEZ,YAAMA,SAAQ;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA,yDAAyD,KAAK,UAAU;AAAA,QACxE;AAAA,QACA;AAAA,QACA;AAAA,MACD,EAAE,KAAK,IAAI;AACX,UAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,UAAI,IAAIA,MAAK;AACb;AAAA,IACD;AAGA,UAAM,QAAQ;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA,6BAA6B,KAAK,UAAU,kBAAkB,MAAM;AAAA,MACpE;AAAA,IACD,EAAE,KAAK,IAAI;AAEX,QAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,QAAI,IAAI,KAAK;AAAA,EACd;AAAA,EAEQ,cAAc,QAAgB,KAAgC;AACrE,YAAQ,IAAI,+CAA+C,MAAM,EAAE;AAEnE,UAAM,QAAQ,WAAW,MAAM;AAE9B,WAAK,iBAAiB,OAAO,MAAM;AACnC,YAAM,QAAQ;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA,6BAA6B,KAAK,UAAU,kBAAkB,MAAM;AAAA,QACpE;AAAA,MACD,EAAE,KAAK,IAAI;AACX,UAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,UAAI,IAAI,KAAK;AAAA,IACd,GAAG,IAAM;AAET,SAAK,iBAAiB,IAAI,QAAQ;AAAA,MACjC,SAAS,CAAC,UAAkB;AAC3B,YAAI,UAAU,KAAK,EAAE,gBAAgB,kBAAkB,CAAC;AACxD,YAAI,IAAI,KAAK;AAAA,MACd;AAAA,MACA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEQ,aAAa,QAAgC,KAAgC;AACpF,UAAM,UAAU,OAAO,WAAW;AAClC,UAAM,aAAa,OAAO,cAAc;AAExC,YAAQ,IAAI,yBAAyB,OAAO,YAAY,UAAU,EAAE;AAEpE,UAAM,OAAO,KAAK,YAAY,IAAI,OAAO;AACzC,QAAI,MAAM;AACT,WAAK,SAAS;AACd,UAAI,eAAe,eAAe,eAAe,YAAY,eAAe,cAAc,eAAe,UAAU,eAAe,aAAa;AAC9I,aAAK,YAAY,OAAO,OAAO;AAAA,MAChC;AAAA,IACD;AAEA,QAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,QAAI,IAAI,IAAI;AAAA,EACb;AAAA;AAAA,EAGQ;AAAA,EAER,cAAc,QAAsB;AACnC,SAAK,aAAa;AAAA,EACnB;AACD;AAGA,SAAS,cAAc,MAAsC;AAC5D,QAAM,SAAiC,CAAC;AACxC,aAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AACnC,UAAM,CAAC,KAAK,GAAG,IAAI,IAAI,KAAK,MAAM,GAAG;AACrC,QAAI,KAAK;AACR,aAAO,mBAAmB,GAAG,CAAC,IAAI,mBAAmB,KAAK,KAAK,GAAG,EAAE,QAAQ,OAAO,GAAG,CAAC;AAAA,IACxF;AAAA,EACD;AACA,SAAO;AACR;AAGA,SAAS,UAAU,KAAqB;AACvC,SAAO,IACL,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AACzB;;;AD3UA,IAAI;AACJ,IAAI;AAGJ,IAAM,eAAe,oBAAI,IAAiD;AAEnE,IAAM,MAAqB;AAAA,EACjC,MAAM;AAAA,EACN,SAAS;AAAA,EACT,aAAa;AAAA,EAEb,OAAO;AAAA,IACN;AAAA,MACC,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY,EAAE,OAAO;AAAA,QACpB,IAAI,EAAE,OAAO,EAAE,SAAS,kDAAkD;AAAA,QAC1E,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,yDAAyD;AAAA,MACnG,CAAC;AAAA,MACD,MAAM,QAAQ,QAAQ;AACrB,cAAM,EAAE,IAAI,SAAS,IAAI;AACzB,YAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,+BAA+B;AAC5D,cAAM,SAAS,MAAM,OAAO,aAAa,IAAI,QAAQ;AACrD,eAAO,EAAE,IAAI,MAAM,UAAU,OAAO,QAAQ;AAAA,MAC7C;AAAA,IACD;AAAA,IACA;AAAA,MACC,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY,EAAE,OAAO;AAAA,QACpB,UAAU,EAAE,OAAO,EAAE,SAAS,wCAAwC;AAAA,MACvE,CAAC;AAAA,MACD,MAAM,QAAQ,QAAQ;AACrB,cAAM,EAAE,SAAS,IAAI;AACrB,YAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,+BAA+B;AAC5D,cAAM,OAAO,QAAQ,QAAQ;AAC7B,eAAO,EAAE,IAAI,KAAK;AAAA,MACnB;AAAA,IACD;AAAA,IACA;AAAA,MACC,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY,EAAE,OAAO,CAAC,CAAC;AAAA,MACvB,MAAM,UAAU;AACf,YAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,+BAA+B;AAC5D,cAAM,QAAQ,MAAM,KAAK,OAAO,YAAY,OAAO,CAAC;AACpD,eAAO,EAAE,IAAI,MAAM,MAAM;AAAA,MAC1B;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAM,SAAS;AACd,UAAM,aAAa,QAAQ,IAAI;AAC/B,UAAM,YAAY,QAAQ,IAAI;AAC9B,UAAM,aAAa,QAAQ,IAAI;AAC/B,UAAM,aAAa,QAAQ,IAAI;AAC/B,UAAM,OAAO,QAAQ,IAAI,kBAAkB,OAAO,SAAS,QAAQ,IAAI,iBAAiB,EAAE,IAAI;AAE9F,QAAI,CAAC,cAAc,CAAC,aAAa,CAAC,cAAc,CAAC,YAAY;AAC5D,cAAQ;AAAA,QACP;AAAA,MACD;AACA;AAAA,IACD;AAEA,gBAAY,mBAAmB;AAG/B,cAAU,UAAU,CAAC,kBAAkB,aAAa,CAAC;AAErD,cAAU,WAAW,CAAC,OAAO,SAAS;AACrC,YAAM,WAAW;AAKjB,YAAM,SAAS,UAAU;AACzB,UAAI,CAAC,OAAQ;AAEb,YAAM,SAAS,aAAa,IAAI,MAAM;AACtC,UAAI,CAAC,OAAQ;AAEb,mBAAa,OAAO,MAAM;AAE1B,UAAI,UAAU,oBAAoB,SAAS,QAAQ;AAClD,gBAAQ,cAAc,QAAQ,SAAS,MAAM;AAAA,MAC9C,WAAW,UAAU,eAAe;AACnC,cAAM,WAAW,SAAS,SAAS;AACnC,gBAAQ,cAAc,QAAQ,QAAQ;AAAA,MACvC;AAAA,IACD,CAAC;AAED,QAAI;AACH,eAAS,IAAI,aAAa,YAAY,WAAW,YAAY,YAAY,IAAI;AAE7E,aAAO,SAAS,OAAO,MAAM,SAAS,SAAS;AAC9C,gBAAQ,IAAI,gCAAgC,IAAI,YAAY,OAAO,KAAK,IAAI,EAAE;AAE9E,YAAI;AACH,gBAAM,EAAE,OAAO,IAAI,MAAM,UAAW,WAAW,MAAM;AAAA,YACpD,WAAW,QAAQ,OAAO;AAAA,YAC1B,QAAQ;AAAA,YACR;AAAA,YACA;AAAA,UACD,CAAC;AAED,uBAAa,IAAI,QAAQ,EAAE,SAAS,OAAO,CAAC;AAC5C,iBAAQ,cAAc,MAAM;AAAA,QAC7B,SAAS,KAAK;AACb,kBAAQ,MAAM,2CAA2C,GAAG;AAAA,QAC7D;AAAA,MACD,CAAC;AAED,YAAM,OAAO,MAAM;AAAA,IACpB,SAAS,KAAK;AACb,cAAQ,MAAM,mDAAmD,GAAG;AACpE,eAAS;AAAA,IACV;AAAA,EACD;AAAA,EAEA,MAAM,WAAW;AAChB,QAAI,QAAQ;AACX,YAAM,OAAO,KAAK;AAClB,eAAS;AAAA,IACV;AACA,gBAAY;AACZ,iBAAa,MAAM;AAAA,EACpB;AACD;;;ADjIA,IAAO,gBAAQ,UAAU,GAAG;","names":["twiml"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openvole/paw-voice-call",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Voice call channel Paw for OpenVole — Twilio voice conversations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"twilio": "^5.4.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^22.0.0",
|
|
16
|
+
"tsup": "^8.3.0",
|
|
17
|
+
"typescript": "^5.6.0",
|
|
18
|
+
"@openvole/paw-sdk": "^0.3.0"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"vole-paw.json",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@openvole/paw-sdk": "^0.3.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/vole-paw.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openvole/paw-voice-call",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Voice call channel for OpenVole via Twilio",
|
|
5
|
+
"entry": "./dist/index.js",
|
|
6
|
+
"brain": false,
|
|
7
|
+
"inProcess": false,
|
|
8
|
+
"transport": "ipc",
|
|
9
|
+
"tools": [
|
|
10
|
+
{ "name": "initiate_call", "description": "Initiate an outbound phone call" },
|
|
11
|
+
{ "name": "end_call", "description": "End an active phone call" },
|
|
12
|
+
{ "name": "list_calls", "description": "List active phone calls" }
|
|
13
|
+
],
|
|
14
|
+
"permissions": {
|
|
15
|
+
"network": ["api.twilio.com", "*.twilio.com"],
|
|
16
|
+
"listen": [3979],
|
|
17
|
+
"filesystem": [],
|
|
18
|
+
"env": ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_PHONE_NUMBER", "VOICE_CALL_WEBHOOK_URL", "VOICE_CALL_PORT"]
|
|
19
|
+
}
|
|
20
|
+
}
|