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.
Files changed (96) hide show
  1. package/.env.example +108 -0
  2. package/Dockerfile +84 -0
  3. package/LICENSE +22 -0
  4. package/README.md +231 -0
  5. package/claude-api-server/package-lock.json +832 -0
  6. package/claude-api-server/package.json +20 -0
  7. package/claude-api-server/server.js +540 -0
  8. package/claude-api-server/structured.js +208 -0
  9. package/cli/README.md +231 -0
  10. package/cli/bin/check-publish-safety.js +53 -0
  11. package/cli/bin/claude-phone.js +23 -0
  12. package/cli/bin/cli-main.js +284 -0
  13. package/cli/bin/postinstall.js +20 -0
  14. package/cli/lib/commands/api-server.js +77 -0
  15. package/cli/lib/commands/backup.js +69 -0
  16. package/cli/lib/commands/config/path.js +30 -0
  17. package/cli/lib/commands/config/reset.js +73 -0
  18. package/cli/lib/commands/config/show.js +86 -0
  19. package/cli/lib/commands/device/add.js +143 -0
  20. package/cli/lib/commands/device/list.js +55 -0
  21. package/cli/lib/commands/device/remove.js +83 -0
  22. package/cli/lib/commands/doctor.js +422 -0
  23. package/cli/lib/commands/logs.js +186 -0
  24. package/cli/lib/commands/restore.js +172 -0
  25. package/cli/lib/commands/setup.js +1246 -0
  26. package/cli/lib/commands/start.js +346 -0
  27. package/cli/lib/commands/status.js +139 -0
  28. package/cli/lib/commands/stop.js +101 -0
  29. package/cli/lib/commands/uninstall.js +183 -0
  30. package/cli/lib/commands/update.js +205 -0
  31. package/cli/lib/config.js +113 -0
  32. package/cli/lib/docker.js +384 -0
  33. package/cli/lib/mcp-register.js +108 -0
  34. package/cli/lib/network.js +109 -0
  35. package/cli/lib/platform.js +85 -0
  36. package/cli/lib/port-check.js +135 -0
  37. package/cli/lib/prereqs/checks/compose.js +147 -0
  38. package/cli/lib/prereqs/checks/disk.js +89 -0
  39. package/cli/lib/prereqs/checks/docker.js +155 -0
  40. package/cli/lib/prereqs/checks/network.js +78 -0
  41. package/cli/lib/prereqs/checks/node.js +95 -0
  42. package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
  43. package/cli/lib/prereqs/installers/docker.js +229 -0
  44. package/cli/lib/prereqs/installers/node.js +254 -0
  45. package/cli/lib/prereqs/platform.js +175 -0
  46. package/cli/lib/prereqs/utils/execute.js +208 -0
  47. package/cli/lib/prereqs/utils/rollback.js +223 -0
  48. package/cli/lib/prereqs/utils/sudo.js +177 -0
  49. package/cli/lib/prereqs.js +228 -0
  50. package/cli/lib/prerequisites.js +115 -0
  51. package/cli/lib/process-manager.js +176 -0
  52. package/cli/lib/utils.js +78 -0
  53. package/cli/lib/validators.js +219 -0
  54. package/cli/lib/voice-downloader.js +79 -0
  55. package/docker/entrypoint.sh +110 -0
  56. package/docker/supervisord.conf +78 -0
  57. package/docker-compose.yml +40 -0
  58. package/docs/CLAUDE-CODE-SKILL.md +45 -0
  59. package/docs/LANGUAGES.md +113 -0
  60. package/docs/MCP-SERVER.md +111 -0
  61. package/docs/TROUBLESHOOTING.md +288 -0
  62. package/mcp-server/index.js +159 -0
  63. package/mcp-server/package-lock.json +1201 -0
  64. package/mcp-server/package.json +9 -0
  65. package/package.json +73 -0
  66. package/stt-local/Dockerfile +12 -0
  67. package/stt-local/server.py +45 -0
  68. package/tts-local/Dockerfile +9 -0
  69. package/tts-local/server.py +83 -0
  70. package/voice-app/API-QUERY-CONTRACT.md +238 -0
  71. package/voice-app/DEPLOYMENT.md +176 -0
  72. package/voice-app/Dockerfile +20 -0
  73. package/voice-app/README-OUTBOUND.md +316 -0
  74. package/voice-app/config/devices.json.example +18 -0
  75. package/voice-app/index.js +273 -0
  76. package/voice-app/lib/audio-fork.js +465 -0
  77. package/voice-app/lib/claude-bridge.js +113 -0
  78. package/voice-app/lib/connection-retry.js +58 -0
  79. package/voice-app/lib/conversation-loop.js +490 -0
  80. package/voice-app/lib/device-registry.js +171 -0
  81. package/voice-app/lib/http-server.js +242 -0
  82. package/voice-app/lib/logger.js +35 -0
  83. package/voice-app/lib/multi-registrar.js +133 -0
  84. package/voice-app/lib/outbound-handler.js +299 -0
  85. package/voice-app/lib/outbound-routes.js +417 -0
  86. package/voice-app/lib/outbound-session.js +324 -0
  87. package/voice-app/lib/query-routes.js +484 -0
  88. package/voice-app/lib/registrar.js +144 -0
  89. package/voice-app/lib/sip-handler.js +490 -0
  90. package/voice-app/lib/tts-service.js +205 -0
  91. package/voice-app/lib/whisper-client.js +140 -0
  92. package/voice-app/package.json +38 -0
  93. package/voice-app/static/gotit-beep.wav +0 -0
  94. package/voice-app/static/hold-music.wav +0 -0
  95. package/voice-app/static/ready-beep.wav +0 -0
  96. 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"); });