claude-phone-local 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +108 -0
- package/Dockerfile +84 -0
- package/LICENSE +22 -0
- package/README.md +231 -0
- package/claude-api-server/package-lock.json +832 -0
- package/claude-api-server/package.json +20 -0
- package/claude-api-server/server.js +540 -0
- package/claude-api-server/structured.js +208 -0
- package/cli/README.md +231 -0
- package/cli/bin/check-publish-safety.js +53 -0
- package/cli/bin/claude-phone.js +23 -0
- package/cli/bin/cli-main.js +284 -0
- package/cli/bin/postinstall.js +20 -0
- package/cli/lib/commands/api-server.js +77 -0
- package/cli/lib/commands/backup.js +69 -0
- package/cli/lib/commands/config/path.js +30 -0
- package/cli/lib/commands/config/reset.js +73 -0
- package/cli/lib/commands/config/show.js +86 -0
- package/cli/lib/commands/device/add.js +143 -0
- package/cli/lib/commands/device/list.js +55 -0
- package/cli/lib/commands/device/remove.js +83 -0
- package/cli/lib/commands/doctor.js +422 -0
- package/cli/lib/commands/logs.js +186 -0
- package/cli/lib/commands/restore.js +172 -0
- package/cli/lib/commands/setup.js +1246 -0
- package/cli/lib/commands/start.js +346 -0
- package/cli/lib/commands/status.js +139 -0
- package/cli/lib/commands/stop.js +101 -0
- package/cli/lib/commands/uninstall.js +183 -0
- package/cli/lib/commands/update.js +205 -0
- package/cli/lib/config.js +113 -0
- package/cli/lib/docker.js +384 -0
- package/cli/lib/mcp-register.js +108 -0
- package/cli/lib/network.js +109 -0
- package/cli/lib/platform.js +85 -0
- package/cli/lib/port-check.js +135 -0
- package/cli/lib/prereqs/checks/compose.js +147 -0
- package/cli/lib/prereqs/checks/disk.js +89 -0
- package/cli/lib/prereqs/checks/docker.js +155 -0
- package/cli/lib/prereqs/checks/network.js +78 -0
- package/cli/lib/prereqs/checks/node.js +95 -0
- package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
- package/cli/lib/prereqs/installers/docker.js +229 -0
- package/cli/lib/prereqs/installers/node.js +254 -0
- package/cli/lib/prereqs/platform.js +175 -0
- package/cli/lib/prereqs/utils/execute.js +208 -0
- package/cli/lib/prereqs/utils/rollback.js +223 -0
- package/cli/lib/prereqs/utils/sudo.js +177 -0
- package/cli/lib/prereqs.js +228 -0
- package/cli/lib/prerequisites.js +115 -0
- package/cli/lib/process-manager.js +176 -0
- package/cli/lib/utils.js +78 -0
- package/cli/lib/validators.js +219 -0
- package/cli/lib/voice-downloader.js +79 -0
- package/docker/entrypoint.sh +110 -0
- package/docker/supervisord.conf +78 -0
- package/docker-compose.yml +40 -0
- package/docs/CLAUDE-CODE-SKILL.md +45 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/MCP-SERVER.md +111 -0
- package/docs/TROUBLESHOOTING.md +288 -0
- package/mcp-server/index.js +159 -0
- package/mcp-server/package-lock.json +1201 -0
- package/mcp-server/package.json +9 -0
- package/package.json +73 -0
- package/stt-local/Dockerfile +12 -0
- package/stt-local/server.py +45 -0
- package/tts-local/Dockerfile +9 -0
- package/tts-local/server.py +83 -0
- package/voice-app/API-QUERY-CONTRACT.md +238 -0
- package/voice-app/DEPLOYMENT.md +176 -0
- package/voice-app/Dockerfile +20 -0
- package/voice-app/README-OUTBOUND.md +316 -0
- package/voice-app/config/devices.json.example +18 -0
- package/voice-app/index.js +273 -0
- package/voice-app/lib/audio-fork.js +465 -0
- package/voice-app/lib/claude-bridge.js +113 -0
- package/voice-app/lib/connection-retry.js +58 -0
- package/voice-app/lib/conversation-loop.js +490 -0
- package/voice-app/lib/device-registry.js +171 -0
- package/voice-app/lib/http-server.js +242 -0
- package/voice-app/lib/logger.js +35 -0
- package/voice-app/lib/multi-registrar.js +133 -0
- package/voice-app/lib/outbound-handler.js +299 -0
- package/voice-app/lib/outbound-routes.js +417 -0
- package/voice-app/lib/outbound-session.js +324 -0
- package/voice-app/lib/query-routes.js +484 -0
- package/voice-app/lib/registrar.js +144 -0
- package/voice-app/lib/sip-handler.js +490 -0
- package/voice-app/lib/tts-service.js +205 -0
- package/voice-app/lib/whisper-client.js +140 -0
- package/voice-app/package.json +38 -0
- package/voice-app/static/gotit-beep.wav +0 -0
- package/voice-app/static/hold-music.wav +0 -0
- package/voice-app/static/ready-beep.wav +0 -0
- package/voice-app/test/freeswitch-retry.test.js +100 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Outbound Calling API
|
|
2
|
+
|
|
3
|
+
> **Using this from Claude Code?** Prefer the MCP server -- it wraps these
|
|
4
|
+
> endpoints as native tools (`call_me`, `call_status`, `hangup`) so Claude can
|
|
5
|
+
> phone you without you writing any HTTP calls. See
|
|
6
|
+
> [../docs/MCP-SERVER.md](../docs/MCP-SERVER.md). This document is the raw HTTP
|
|
7
|
+
> API underneath, still fully supported.
|
|
8
|
+
|
|
9
|
+
API reference for initiating outbound calls from Claude Phone.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The outbound calling API allows your server to call phone numbers and deliver messages. Use cases:
|
|
14
|
+
|
|
15
|
+
- Server alerts ("Your disk is 95% full")
|
|
16
|
+
- Automated notifications
|
|
17
|
+
- Two-way conversations triggered by events
|
|
18
|
+
|
|
19
|
+
## Endpoints
|
|
20
|
+
|
|
21
|
+
### POST /api/outbound-call
|
|
22
|
+
|
|
23
|
+
Initiate an outbound call.
|
|
24
|
+
|
|
25
|
+
**Request:**
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"to": "+15551234567",
|
|
30
|
+
"message": "Hello from your server",
|
|
31
|
+
"mode": "announce",
|
|
32
|
+
"device": "Morpheus",
|
|
33
|
+
"callerId": "+15559876543",
|
|
34
|
+
"timeoutSeconds": 30,
|
|
35
|
+
"webhookUrl": "https://example.com/webhook"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Field | Required | Description |
|
|
40
|
+
|-------|----------|-------------|
|
|
41
|
+
| `to` | Yes | Phone number in E.164 format |
|
|
42
|
+
| `message` | Yes | Text to speak (max 1000 chars) |
|
|
43
|
+
| `mode` | No | `announce` (default) or `conversation` |
|
|
44
|
+
| `device` | No | Device name for voice/personality |
|
|
45
|
+
| `callerId` | No | Caller ID to display |
|
|
46
|
+
| `timeoutSeconds` | No | Ring timeout 5-120 (default: 30) |
|
|
47
|
+
| `webhookUrl` | No | URL for status callbacks |
|
|
48
|
+
|
|
49
|
+
**Response:**
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"success": true,
|
|
54
|
+
"callId": "abc123-uuid",
|
|
55
|
+
"status": "queued",
|
|
56
|
+
"message": "Call initiated"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### GET /api/call/:callId
|
|
61
|
+
|
|
62
|
+
Get status of a specific call.
|
|
63
|
+
|
|
64
|
+
**Response:**
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"success": true,
|
|
69
|
+
"callId": "abc123-uuid",
|
|
70
|
+
"to": "+15551234567",
|
|
71
|
+
"state": "completed",
|
|
72
|
+
"mode": "announce",
|
|
73
|
+
"createdAt": "2025-01-01T12:00:00.000Z",
|
|
74
|
+
"answeredAt": "2025-01-01T12:00:05.234Z",
|
|
75
|
+
"endedAt": "2025-01-01T12:00:15.678Z",
|
|
76
|
+
"duration": 10
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### GET /api/calls
|
|
81
|
+
|
|
82
|
+
List all active calls.
|
|
83
|
+
|
|
84
|
+
**Response:**
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"success": true,
|
|
89
|
+
"count": 2,
|
|
90
|
+
"calls": [
|
|
91
|
+
{ "callId": "...", "to": "...", "state": "playing" },
|
|
92
|
+
{ "callId": "...", "to": "...", "state": "dialing" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### POST /api/call/:callId/hangup
|
|
98
|
+
|
|
99
|
+
Manually hang up an active call.
|
|
100
|
+
|
|
101
|
+
**Response:**
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"success": true,
|
|
106
|
+
"message": "Call hangup initiated",
|
|
107
|
+
"callId": "abc123-uuid"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Call States
|
|
112
|
+
|
|
113
|
+
| State | Description |
|
|
114
|
+
|-------|-------------|
|
|
115
|
+
| `queued` | Call created, not yet dialing |
|
|
116
|
+
| `dialing` | SIP INVITE sent, waiting for answer |
|
|
117
|
+
| `playing` | Call answered, playing message |
|
|
118
|
+
| `completed` | Call finished successfully |
|
|
119
|
+
| `failed` | Call failed (busy, no answer, error) |
|
|
120
|
+
|
|
121
|
+
## Call Modes
|
|
122
|
+
|
|
123
|
+
### Announce Mode (Default)
|
|
124
|
+
|
|
125
|
+
Plays the message and hangs up:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
curl -X POST http://localhost:3000/api/outbound-call \
|
|
129
|
+
-H "Content-Type: application/json" \
|
|
130
|
+
-d '{
|
|
131
|
+
"to": "+15551234567",
|
|
132
|
+
"message": "Alert: Your server storage is at 95 percent."
|
|
133
|
+
}'
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Conversation Mode
|
|
137
|
+
|
|
138
|
+
Plays the message, then allows back-and-forth conversation:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
curl -X POST http://localhost:3000/api/outbound-call \
|
|
142
|
+
-H "Content-Type: application/json" \
|
|
143
|
+
-d '{
|
|
144
|
+
"to": "+15551234567",
|
|
145
|
+
"message": "Alert: Your server storage is at 95 percent. Would you like me to clean up old logs?",
|
|
146
|
+
"mode": "conversation",
|
|
147
|
+
"device": "Morpheus"
|
|
148
|
+
}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Webhooks
|
|
152
|
+
|
|
153
|
+
If `webhookUrl` is provided, POST requests are sent on state changes:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"callId": "abc123-uuid",
|
|
158
|
+
"state": "completed",
|
|
159
|
+
"to": "+15551234567",
|
|
160
|
+
"duration": 15,
|
|
161
|
+
"timestamp": "2025-01-01T12:00:15.678Z"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Error Responses
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"success": false,
|
|
170
|
+
"error": "Invalid phone number format"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| Error | Cause |
|
|
175
|
+
|-------|-------|
|
|
176
|
+
| `Invalid phone number format` | `to` not in E.164 format |
|
|
177
|
+
| `Message is required` | Missing `message` field |
|
|
178
|
+
| `Message too long` | Message exceeds 1000 chars |
|
|
179
|
+
| `Call not found` | Invalid `callId` |
|
|
180
|
+
| `service_unavailable` | SIP/media server not ready |
|
|
181
|
+
|
|
182
|
+
## Failure Reasons
|
|
183
|
+
|
|
184
|
+
When a call fails, the `reason` field indicates why:
|
|
185
|
+
|
|
186
|
+
| Reason | Description |
|
|
187
|
+
|--------|-------------|
|
|
188
|
+
| `busy` | Recipient busy (SIP 486) |
|
|
189
|
+
| `no_answer` | No answer within timeout (SIP 480/408) |
|
|
190
|
+
| `not_found` | Number not found (SIP 404) |
|
|
191
|
+
| `rejected` | Call rejected (SIP 603) |
|
|
192
|
+
| `service_unavailable` | Server error (SIP 503) |
|
|
193
|
+
|
|
194
|
+
## Examples
|
|
195
|
+
|
|
196
|
+
### Basic Alert
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
curl -X POST http://localhost:3000/api/outbound-call \
|
|
200
|
+
-H "Content-Type: application/json" \
|
|
201
|
+
-d '{
|
|
202
|
+
"to": "+15551234567",
|
|
203
|
+
"message": "Your backup job completed successfully."
|
|
204
|
+
}'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### With Webhook
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
curl -X POST http://localhost:3000/api/outbound-call \
|
|
211
|
+
-H "Content-Type: application/json" \
|
|
212
|
+
-d '{
|
|
213
|
+
"to": "+15551234567",
|
|
214
|
+
"message": "Server alert: High CPU usage detected.",
|
|
215
|
+
"webhookUrl": "https://n8n.example.com/webhook/call-status"
|
|
216
|
+
}'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Check Status
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
# Get call status
|
|
223
|
+
curl http://localhost:3000/api/call/abc123-uuid
|
|
224
|
+
|
|
225
|
+
# List all active calls
|
|
226
|
+
curl http://localhost:3000/api/calls
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Integration Examples
|
|
230
|
+
|
|
231
|
+
### Home Assistant
|
|
232
|
+
|
|
233
|
+
```yaml
|
|
234
|
+
rest_command:
|
|
235
|
+
call_alert:
|
|
236
|
+
url: "http://VOICE_SERVER:3000/api/outbound-call"
|
|
237
|
+
method: POST
|
|
238
|
+
content_type: "application/json"
|
|
239
|
+
payload: '{"to": "+15551234567", "message": "{{ message }}"}'
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### n8n Workflow: Automated Alert System
|
|
243
|
+
|
|
244
|
+
This example workflow queries a device for status, then calls you if there's a problem.
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
|
|
248
|
+
│ Trigger │ ──▶ │ Query Device │ ──▶ │ If Alert │ ──▶ │ Call User │
|
|
249
|
+
│ (Schedule) │ │ POST /query │ │ Needed │ │ POST /call │
|
|
250
|
+
└──────────────┘ └──────────────┘ └──────────┘ └──────────────┘
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Node 1: Trigger**
|
|
254
|
+
- Use Schedule Trigger (e.g., every 5 minutes) or Manual Trigger for testing
|
|
255
|
+
|
|
256
|
+
**Node 2: HTTP Request - Query Device**
|
|
257
|
+
```
|
|
258
|
+
Method: POST
|
|
259
|
+
URL: http://YOUR_SERVER:3000/api/query
|
|
260
|
+
Body (JSON):
|
|
261
|
+
```
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"query": "Check the system health. Alert me if anything needs attention.",
|
|
265
|
+
"device": "Morpheus",
|
|
266
|
+
"format": "json",
|
|
267
|
+
"schema": {
|
|
268
|
+
"queryType": "alert_check",
|
|
269
|
+
"requiredFields": ["call_user", "reason"],
|
|
270
|
+
"fieldGuidance": {
|
|
271
|
+
"call_user": "Boolean - true if something needs attention",
|
|
272
|
+
"reason": "One sentence explaining the situation, under 30 words"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"timeout": 120
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Node 3: If - Check Alert Condition**
|
|
280
|
+
```
|
|
281
|
+
Condition: {{ $json.structured.call_user }} equals true
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Node 4: HTTP Request - Make Call (True branch only)**
|
|
285
|
+
```
|
|
286
|
+
Method: POST
|
|
287
|
+
URL: http://YOUR_SERVER:3000/api/outbound-call
|
|
288
|
+
Body (JSON):
|
|
289
|
+
```
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"to": "YOUR_EXTENSION",
|
|
293
|
+
"message": "Hey! {{ $json.structured.reason }}. Want details?",
|
|
294
|
+
"mode": "conversation",
|
|
295
|
+
"device": "Morpheus"
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**How it works:**
|
|
300
|
+
1. Trigger fires (on schedule or manually)
|
|
301
|
+
2. Queries your device with a structured schema request
|
|
302
|
+
3. Device returns JSON with `call_user: true/false` and `reason`
|
|
303
|
+
4. If `call_user` is true, initiates outbound call in conversation mode
|
|
304
|
+
5. You answer and can ask follow-up questions
|
|
305
|
+
|
|
306
|
+
### Shell Script
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
#!/bin/bash
|
|
310
|
+
PHONE="+15551234567"
|
|
311
|
+
MESSAGE="Disk space critical on server1"
|
|
312
|
+
|
|
313
|
+
curl -s -X POST http://localhost:3000/api/outbound-call \
|
|
314
|
+
-H "Content-Type: application/json" \
|
|
315
|
+
-d "{\"to\": \"$PHONE\", \"message\": \"$MESSAGE\"}"
|
|
316
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"9000": {
|
|
3
|
+
"name": "Assistant",
|
|
4
|
+
"extension": "9000",
|
|
5
|
+
"authId": "your-3cx-auth-id",
|
|
6
|
+
"password": "your-3cx-password",
|
|
7
|
+
"voiceId": "your-elevenlabs-voice-id",
|
|
8
|
+
"prompt": "You are a helpful AI assistant. Keep voice responses under 40 words."
|
|
9
|
+
},
|
|
10
|
+
"9002": {
|
|
11
|
+
"name": "ServerBot",
|
|
12
|
+
"extension": "9002",
|
|
13
|
+
"authId": "another-auth-id",
|
|
14
|
+
"password": "another-password",
|
|
15
|
+
"voiceId": "another-voice-id",
|
|
16
|
+
"prompt": "You are a server monitoring assistant. Report on system health and alerts. Keep voice responses under 40 words."
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice Interface Application
|
|
3
|
+
* Main entry point - v9 with Multi-Extension + Query API Support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
require("dotenv").config();
|
|
7
|
+
var Srf = require("drachtio-srf");
|
|
8
|
+
var Mrf = require("drachtio-fsmrf");
|
|
9
|
+
|
|
10
|
+
// Import application modules
|
|
11
|
+
var httpServerModule = require("./lib/http-server");
|
|
12
|
+
var createHttpServer = httpServerModule.createHttpServer;
|
|
13
|
+
var cleanupOldFiles = httpServerModule.cleanupOldFiles;
|
|
14
|
+
var AudioForkServer = require("./lib/audio-fork").AudioForkServer;
|
|
15
|
+
var sipHandler = require("./lib/sip-handler");
|
|
16
|
+
var handleInvite = sipHandler.handleInvite;
|
|
17
|
+
var extractCallerId = sipHandler.extractCallerId;
|
|
18
|
+
var whisperClient = require("./lib/whisper-client");
|
|
19
|
+
var claudeBridge = require("./lib/claude-bridge");
|
|
20
|
+
var ttsService = require("./lib/tts-service");
|
|
21
|
+
|
|
22
|
+
// Multi-extension support
|
|
23
|
+
var deviceRegistry = require("./lib/device-registry");
|
|
24
|
+
var MultiRegistrar = require("./lib/multi-registrar");
|
|
25
|
+
|
|
26
|
+
// Connection retry utility
|
|
27
|
+
var connectionRetry = require("./lib/connection-retry");
|
|
28
|
+
var connectWithRetry = connectionRetry.connectWithRetry;
|
|
29
|
+
|
|
30
|
+
// Import outbound calling routes
|
|
31
|
+
var outboundModule = require("./lib/outbound-routes");
|
|
32
|
+
var outboundRouter = outboundModule.router;
|
|
33
|
+
var setupOutboundRoutes = outboundModule.setupRoutes;
|
|
34
|
+
|
|
35
|
+
// Import query routes
|
|
36
|
+
var queryModule = require("./lib/query-routes");
|
|
37
|
+
var queryRouter = queryModule.router;
|
|
38
|
+
var setupQueryRoutes = queryModule.setupRoutes;
|
|
39
|
+
|
|
40
|
+
// Load device registry first
|
|
41
|
+
// deviceRegistry is a singleton, already instantiated
|
|
42
|
+
|
|
43
|
+
// Configuration
|
|
44
|
+
var config = {
|
|
45
|
+
drachtio: {
|
|
46
|
+
host: process.env.DRACHTIO_HOST || "drachtio",
|
|
47
|
+
port: parseInt(process.env.DRACHTIO_PORT) || 9022,
|
|
48
|
+
secret: process.env.DRACHTIO_SECRET || "cymru"
|
|
49
|
+
},
|
|
50
|
+
freeswitch: {
|
|
51
|
+
host: process.env.FREESWITCH_HOST || "freeswitch",
|
|
52
|
+
port: parseInt(process.env.FREESWITCH_PORT) || 8021,
|
|
53
|
+
secret: process.env.FREESWITCH_SECRET || "JambonzR0ck$"
|
|
54
|
+
},
|
|
55
|
+
sip: {
|
|
56
|
+
extension: process.env.SIP_EXTENSION || "9000",
|
|
57
|
+
auth_id: process.env.SIP_AUTH_ID || "Au0XZPTpJY",
|
|
58
|
+
password: process.env.SIP_AUTH_PASSWORD || "DGHwMW6v25",
|
|
59
|
+
domain: process.env.SIP_DOMAIN || "your3cx.3cx.us",
|
|
60
|
+
registrar: process.env.SIP_REGISTRAR || "your3cx.3cx.us",
|
|
61
|
+
registrar_port: parseInt(process.env.SIP_REGISTRAR_PORT) || 5060,
|
|
62
|
+
expiry: parseInt(process.env.SIP_EXPIRY) || 3600
|
|
63
|
+
},
|
|
64
|
+
external_ip: process.env.EXTERNAL_IP || "10.70.7.81",
|
|
65
|
+
http_port: parseInt(process.env.HTTP_PORT) || 3000,
|
|
66
|
+
ws_port: parseInt(process.env.WS_PORT) || 3001,
|
|
67
|
+
audio_dir: process.env.AUDIO_DIR || "/tmp/voice-audio"
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Initialize drachtio SRF
|
|
71
|
+
var srf = new Srf();
|
|
72
|
+
var mediaServer = null;
|
|
73
|
+
var httpServer = null;
|
|
74
|
+
var audioForkServer = null;
|
|
75
|
+
var registrar = null;
|
|
76
|
+
var drachtioConnected = false;
|
|
77
|
+
var freeswitchConnected = false;
|
|
78
|
+
var isReady = false;
|
|
79
|
+
|
|
80
|
+
// Log startup
|
|
81
|
+
console.log("\n" + "=".repeat(64));
|
|
82
|
+
console.log(" Voice Interface Application Starting ");
|
|
83
|
+
console.log(" (with Multi-Extension + Query API Support) ");
|
|
84
|
+
console.log("=".repeat(64));
|
|
85
|
+
console.log("\nConfiguration:");
|
|
86
|
+
console.log(" - drachtio: " + config.drachtio.host + ":" + config.drachtio.port);
|
|
87
|
+
console.log(" - FreeSWITCH: " + config.freeswitch.host + ":" + config.freeswitch.port);
|
|
88
|
+
console.log(" - SIP Domain: " + config.sip.domain);
|
|
89
|
+
console.log(" - Registrar: " + config.sip.registrar + ":" + config.sip.registrar_port);
|
|
90
|
+
console.log(" - External IP: " + config.external_ip);
|
|
91
|
+
console.log(" - HTTP Port: " + config.http_port);
|
|
92
|
+
console.log(" - WS Port: " + config.ws_port);
|
|
93
|
+
console.log(" - Audio Dir: " + config.audio_dir);
|
|
94
|
+
console.log(" - Mix Type: " + (process.env.AUDIO_FORK_MIXTYPE || "L") + " (capture direction)");
|
|
95
|
+
console.log("\n[DEVICES] Loaded " + Object.keys(deviceRegistry.getAllDevices()).length + " device extensions");
|
|
96
|
+
console.log("\nWaiting for connections...\n");
|
|
97
|
+
|
|
98
|
+
// Connect to drachtio
|
|
99
|
+
srf.connect({
|
|
100
|
+
host: config.drachtio.host,
|
|
101
|
+
port: config.drachtio.port,
|
|
102
|
+
secret: config.drachtio.secret
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
srf.on("connect", function(err, hostport) {
|
|
106
|
+
console.log("[" + new Date().toISOString() + "] DRACHTIO Connected at " + hostport);
|
|
107
|
+
drachtioConnected = true;
|
|
108
|
+
|
|
109
|
+
var localAddress = config.external_ip;
|
|
110
|
+
if (hostport && hostport.length > 0) {
|
|
111
|
+
var match = hostport[0].match(/\/([^:]+)/);
|
|
112
|
+
if (match) localAddress = match[1];
|
|
113
|
+
}
|
|
114
|
+
console.log("[DRACHTIO] Local SIP address: " + localAddress);
|
|
115
|
+
|
|
116
|
+
// Start Multi-Registration for all devices
|
|
117
|
+
if (!registrar) {
|
|
118
|
+
registrar = new MultiRegistrar(srf, {
|
|
119
|
+
domain: config.sip.domain,
|
|
120
|
+
registrar: config.sip.registrar,
|
|
121
|
+
registrar_port: config.sip.registrar_port,
|
|
122
|
+
local_address: localAddress,
|
|
123
|
+
local_port: parseInt(process.env.DRACHTIO_SIP_PORT) || 5060,
|
|
124
|
+
expiry: config.sip.expiry
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Register all devices from config
|
|
128
|
+
registrar.registerAll(deviceRegistry.getRegistrationConfigs());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
checkReadyState();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
srf.on("error", function(err) {
|
|
135
|
+
console.error("[" + new Date().toISOString() + "] DRACHTIO error: " + err.message);
|
|
136
|
+
drachtioConnected = false;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Initialize FreeSWITCH MRF with retry logic
|
|
140
|
+
var mrf = new Mrf(srf);
|
|
141
|
+
|
|
142
|
+
// Define FreeSWITCH connection function
|
|
143
|
+
function connectToFreeswitch() {
|
|
144
|
+
return mrf.connect({
|
|
145
|
+
address: config.freeswitch.host,
|
|
146
|
+
port: config.freeswitch.port,
|
|
147
|
+
secret: config.freeswitch.secret
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Connect with exponential backoff retry
|
|
152
|
+
// Retry schedule: 1s, 2s, 3s, 5s, 5s, 5s, 10s, 10s, 10s, 10s (max 10 retries)
|
|
153
|
+
connectWithRetry(connectToFreeswitch, {
|
|
154
|
+
maxRetries: 10,
|
|
155
|
+
retryDelays: [1000, 2000, 3000, 5000, 5000, 5000, 10000, 10000, 10000, 10000],
|
|
156
|
+
name: 'FREESWITCH'
|
|
157
|
+
})
|
|
158
|
+
.then(function(ms) {
|
|
159
|
+
mediaServer = ms;
|
|
160
|
+
freeswitchConnected = true;
|
|
161
|
+
console.log("[" + new Date().toISOString() + "] FREESWITCH Ready for calls");
|
|
162
|
+
checkReadyState();
|
|
163
|
+
})
|
|
164
|
+
.catch(function(err) {
|
|
165
|
+
console.error("[" + new Date().toISOString() + "] FREESWITCH Connection failed permanently: " + err.message);
|
|
166
|
+
console.error("[" + new Date().toISOString() + "] Please check:");
|
|
167
|
+
console.error(" 1. FreeSWITCH container is running: docker ps | grep freeswitch");
|
|
168
|
+
console.error(" 2. ESL port 8021 is accessible");
|
|
169
|
+
console.error(" 3. EXTERNAL_IP is set correctly in .env");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Initialize servers
|
|
174
|
+
function initializeServers() {
|
|
175
|
+
var fs = require("fs");
|
|
176
|
+
if (!fs.existsSync(config.audio_dir)) {
|
|
177
|
+
fs.mkdirSync(config.audio_dir, { recursive: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// HTTP server for TTS audio
|
|
181
|
+
httpServer = createHttpServer(config.audio_dir, config.http_port);
|
|
182
|
+
console.log("[" + new Date().toISOString() + "] HTTP Server started on port " + config.http_port);
|
|
183
|
+
|
|
184
|
+
// WebSocket server for audio fork
|
|
185
|
+
audioForkServer = new AudioForkServer({ port: config.ws_port });
|
|
186
|
+
audioForkServer.start();
|
|
187
|
+
audioForkServer.on("listening", function() {
|
|
188
|
+
console.log("[" + new Date().toISOString() + "] WEBSOCKET Audio fork server started on port " + config.ws_port);
|
|
189
|
+
});
|
|
190
|
+
audioForkServer.on("session", function(session) {
|
|
191
|
+
console.log("[AUDIO] New session for call " + session.callUuid);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// TTS service
|
|
195
|
+
ttsService.setAudioDir(config.audio_dir);
|
|
196
|
+
console.log("[" + new Date().toISOString() + "] TTS Service configured");
|
|
197
|
+
|
|
198
|
+
// ========== OUTBOUND CALLING ROUTES ==========
|
|
199
|
+
setupOutboundRoutes({
|
|
200
|
+
srf: srf,
|
|
201
|
+
mediaServer: mediaServer,
|
|
202
|
+
deviceRegistry: deviceRegistry, // Required for device lookup
|
|
203
|
+
audioForkServer: audioForkServer,
|
|
204
|
+
whisperClient: whisperClient,
|
|
205
|
+
claudeBridge: claudeBridge,
|
|
206
|
+
ttsService: ttsService,
|
|
207
|
+
wsPort: config.ws_port
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
httpServer.app.use("/api", outboundRouter);
|
|
211
|
+
console.log("[" + new Date().toISOString() + "] OUTBOUND Calling API enabled");
|
|
212
|
+
|
|
213
|
+
// ========== QUERY API ROUTES ==========
|
|
214
|
+
setupQueryRoutes({
|
|
215
|
+
claudeBridge: claudeBridge
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
httpServer.app.use("/api", queryRouter);
|
|
219
|
+
console.log("[" + new Date().toISOString() + "] QUERY API enabled (/api/query, /api/devices)");
|
|
220
|
+
|
|
221
|
+
// Finalize HTTP server
|
|
222
|
+
httpServer.finalize();
|
|
223
|
+
|
|
224
|
+
// Cleanup old files periodically
|
|
225
|
+
setInterval(function() {
|
|
226
|
+
cleanupOldFiles(config.audio_dir, 5 * 60 * 1000);
|
|
227
|
+
}, 60 * 1000);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check ready state
|
|
231
|
+
function checkReadyState() {
|
|
232
|
+
if (drachtioConnected && freeswitchConnected && !isReady) {
|
|
233
|
+
isReady = true;
|
|
234
|
+
console.log("\n[" + new Date().toISOString() + "] READY Voice interface is fully connected!");
|
|
235
|
+
console.log("=".repeat(64) + "\n");
|
|
236
|
+
|
|
237
|
+
initializeServers();
|
|
238
|
+
|
|
239
|
+
// Register SIP INVITE handler
|
|
240
|
+
srf.invite(function(req, res) {
|
|
241
|
+
handleInvite(req, res, {
|
|
242
|
+
audioForkServer: audioForkServer,
|
|
243
|
+
mediaServer: mediaServer,
|
|
244
|
+
deviceRegistry: deviceRegistry,
|
|
245
|
+
config: config,
|
|
246
|
+
whisperClient: whisperClient,
|
|
247
|
+
claudeBridge: claudeBridge,
|
|
248
|
+
ttsService: ttsService,
|
|
249
|
+
wsPort: config.ws_port,
|
|
250
|
+
externalIp: config.external_ip
|
|
251
|
+
}).catch(function(err) {
|
|
252
|
+
console.error("[" + new Date().toISOString() + "] CALL Error: " + err.message);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
console.log("[" + new Date().toISOString() + "] SIP INVITE handler registered");
|
|
257
|
+
console.log("[" + new Date().toISOString() + "] Multi-extension voice interface ready!");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Graceful shutdown
|
|
262
|
+
function shutdown(signal) {
|
|
263
|
+
console.log("\n[" + new Date().toISOString() + "] Received " + signal + ", shutting down...");
|
|
264
|
+
if (registrar) registrar.stop();
|
|
265
|
+
if (httpServer) httpServer.close();
|
|
266
|
+
if (audioForkServer) audioForkServer.stop();
|
|
267
|
+
if (mediaServer) mediaServer.disconnect();
|
|
268
|
+
srf.disconnect();
|
|
269
|
+
setTimeout(function() { process.exit(0); }, 1000);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.on("SIGTERM", function() { shutdown("SIGTERM"); });
|
|
273
|
+
process.on("SIGINT", function() { shutdown("SIGINT"); });
|