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,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound Call API Routes
|
|
3
|
+
* Express routes for initiating and managing outbound calls
|
|
4
|
+
* v3: Added context parameter for structured data to Claude
|
|
5
|
+
* Supports both announce (one-way) and conversation (two-way) modes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const express = require('express');
|
|
9
|
+
const router = express.Router();
|
|
10
|
+
const logger = require('./logger');
|
|
11
|
+
const { OutboundSession, getSession, getAllSessions } = require('./outbound-session');
|
|
12
|
+
const { initiateOutboundCall, playMessage, hangupCall } = require('./outbound-handler');
|
|
13
|
+
const { runConversationLoop } = require('./conversation-loop');
|
|
14
|
+
|
|
15
|
+
// Dependencies injected via setupRoutes()
|
|
16
|
+
var srf = null;
|
|
17
|
+
var mediaServer = null;
|
|
18
|
+
var deviceRegistry = null;
|
|
19
|
+
var audioForkServer = null;
|
|
20
|
+
var whisperClient = null;
|
|
21
|
+
var claudeBridge = null;
|
|
22
|
+
var ttsService = null;
|
|
23
|
+
var wsPort = 3001;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate phone number format
|
|
27
|
+
*/
|
|
28
|
+
function isValidPhoneNumber(phoneNumber) {
|
|
29
|
+
if (typeof phoneNumber !== 'string') return false;
|
|
30
|
+
var e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
31
|
+
var dialStringRegex = /^\d{1,15}$/;
|
|
32
|
+
return e164Regex.test(phoneNumber) || dialStringRegex.test(phoneNumber);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate outbound call request
|
|
37
|
+
*/
|
|
38
|
+
function validateRequest(body) {
|
|
39
|
+
if (!body) {
|
|
40
|
+
return { valid: false, error: 'Request body is required' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!body.to) {
|
|
44
|
+
return { valid: false, error: 'Field "to" is required' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isValidPhoneNumber(body.to)) {
|
|
48
|
+
return { valid: false, error: 'Field "to" must be a valid phone number (e.g. +15551234567 or extension 5755)' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!body.message) {
|
|
52
|
+
return { valid: false, error: 'Field "message" is required' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof body.message !== 'string' || body.message.trim().length === 0) {
|
|
56
|
+
return { valid: false, error: 'Field "message" must be a non-empty string' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (body.message.length > 1000) {
|
|
60
|
+
return { valid: false, error: 'Field "message" must be 1000 characters or less' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (body.callerId && !isValidPhoneNumber(body.callerId)) {
|
|
64
|
+
return { valid: false, error: 'Field "callerId" must be a valid E.164 phone number if provided' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (body.mode && !['announce', 'conversation'].includes(body.mode)) {
|
|
68
|
+
return { valid: false, error: 'Field "mode" must be either "announce" or "conversation"' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (body.device !== undefined && typeof body.device !== 'string') {
|
|
72
|
+
return { valid: false, error: 'Field "device" must be a string (extension number or device name)' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Optional: 'context' validation (string or object)
|
|
76
|
+
if (body.context !== undefined) {
|
|
77
|
+
if (typeof body.context !== 'string' && typeof body.context !== 'object') {
|
|
78
|
+
return { valid: false, error: 'Field "context" must be a string or object' };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (body.timeoutSeconds !== undefined) {
|
|
83
|
+
var timeout = Number(body.timeoutSeconds);
|
|
84
|
+
if (!Number.isInteger(timeout) || timeout < 5 || timeout > 120) {
|
|
85
|
+
return { valid: false, error: 'Field "timeoutSeconds" must be an integer between 5 and 120' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { valid: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* POST /api/outbound-call
|
|
94
|
+
* Initiate an outbound call
|
|
95
|
+
*
|
|
96
|
+
* Body parameters:
|
|
97
|
+
* - to: Phone number (required)
|
|
98
|
+
* - message: Initial message to play (required) - what the device SAYS
|
|
99
|
+
* - context: Background data for Claude (optional) - what the device KNOWS
|
|
100
|
+
* - mode: 'announce' or 'conversation' (default: announce)
|
|
101
|
+
* - device: Device extension or name for voice/personality (optional)
|
|
102
|
+
* - callerId: Caller ID (optional)
|
|
103
|
+
* - timeoutSeconds: Ring timeout (optional, default: 30)
|
|
104
|
+
*/
|
|
105
|
+
router.post('/outbound-call', async function(req, res) {
|
|
106
|
+
var startTime = Date.now();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Validate request
|
|
110
|
+
var validation = validateRequest(req.body);
|
|
111
|
+
if (!validation.valid) {
|
|
112
|
+
logger.warn('Invalid outbound call request', {
|
|
113
|
+
error: validation.error,
|
|
114
|
+
body: req.body
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return res.status(400).json({
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'validation_failed',
|
|
120
|
+
message: validation.error
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Extract parameters
|
|
125
|
+
var to = req.body.to;
|
|
126
|
+
var message = req.body.message;
|
|
127
|
+
var context = req.body.context || null; // NEW: structured context for Claude
|
|
128
|
+
var mode = req.body.mode || 'announce';
|
|
129
|
+
var deviceParam = req.body.device;
|
|
130
|
+
var callerId = req.body.callerId;
|
|
131
|
+
var timeoutSeconds = req.body.timeoutSeconds || 30;
|
|
132
|
+
var webhookUrl = req.body.webhookUrl;
|
|
133
|
+
|
|
134
|
+
// Look up device configuration
|
|
135
|
+
var deviceConfig = null;
|
|
136
|
+
if (deviceParam && deviceRegistry) {
|
|
137
|
+
// Use get() which tries extension first, then name (case-insensitive)
|
|
138
|
+
deviceConfig = deviceRegistry.get(deviceParam);
|
|
139
|
+
|
|
140
|
+
if (deviceConfig) {
|
|
141
|
+
logger.info('Device found for outbound call', {
|
|
142
|
+
device: deviceConfig.name,
|
|
143
|
+
extension: deviceConfig.extension,
|
|
144
|
+
voiceId: deviceConfig.voiceId || 'default'
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
logger.warn('Device not found, using default', { requested: deviceParam });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if infrastructure is available
|
|
152
|
+
if (!srf || !mediaServer) {
|
|
153
|
+
logger.error('Infrastructure not ready', {
|
|
154
|
+
srf: !!srf,
|
|
155
|
+
mediaServer: !!mediaServer
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return res.status(503).json({
|
|
159
|
+
success: false,
|
|
160
|
+
error: 'service_unavailable',
|
|
161
|
+
message: 'Voice infrastructure is not ready'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// For conversation mode, check additional dependencies
|
|
166
|
+
if (mode === 'conversation') {
|
|
167
|
+
if (!audioForkServer || !whisperClient || !claudeBridge || !ttsService) {
|
|
168
|
+
logger.error('Conversation mode dependencies not ready', {
|
|
169
|
+
audioForkServer: !!audioForkServer,
|
|
170
|
+
whisperClient: !!whisperClient,
|
|
171
|
+
claudeBridge: !!claudeBridge,
|
|
172
|
+
ttsService: !!ttsService
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return res.status(503).json({
|
|
176
|
+
success: false,
|
|
177
|
+
error: 'service_unavailable',
|
|
178
|
+
message: 'Conversation mode dependencies not ready'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Create session
|
|
184
|
+
var session = new OutboundSession(null, {
|
|
185
|
+
to: to,
|
|
186
|
+
message: message,
|
|
187
|
+
mode: mode,
|
|
188
|
+
callerId: callerId,
|
|
189
|
+
webhookUrl: webhookUrl,
|
|
190
|
+
device: deviceConfig ? deviceConfig.name : null
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
var callId = session.callId;
|
|
194
|
+
|
|
195
|
+
logger.info('Processing outbound call request', {
|
|
196
|
+
callId: callId,
|
|
197
|
+
to: to,
|
|
198
|
+
mode: mode,
|
|
199
|
+
device: deviceConfig ? deviceConfig.name : 'default',
|
|
200
|
+
messageLength: message.length,
|
|
201
|
+
hasContext: !!context
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Return immediately with callId
|
|
205
|
+
res.json({
|
|
206
|
+
success: true,
|
|
207
|
+
callId: callId,
|
|
208
|
+
status: 'queued',
|
|
209
|
+
message: 'Call initiated',
|
|
210
|
+
device: deviceConfig ? deviceConfig.name : null
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Continue asynchronously
|
|
214
|
+
(async function() {
|
|
215
|
+
try {
|
|
216
|
+
session.transition('DIALING');
|
|
217
|
+
|
|
218
|
+
var result = await initiateOutboundCall(srf, mediaServer, {
|
|
219
|
+
to: to,
|
|
220
|
+
message: message,
|
|
221
|
+
callerId: callerId,
|
|
222
|
+
timeoutSeconds: timeoutSeconds,
|
|
223
|
+
deviceConfig: deviceConfig // Pass device for From header display name
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
var dialog = result.dialog;
|
|
227
|
+
var endpoint = result.endpoint;
|
|
228
|
+
|
|
229
|
+
session.setDialog(dialog);
|
|
230
|
+
session.setEndpoint(endpoint);
|
|
231
|
+
session.transition('PLAYING');
|
|
232
|
+
|
|
233
|
+
// Play the initial message with device voice
|
|
234
|
+
var voiceId = (deviceConfig && deviceConfig.voiceId) ? deviceConfig.voiceId : null;
|
|
235
|
+
await playMessage(endpoint, message, { voiceId: voiceId });
|
|
236
|
+
|
|
237
|
+
if (mode === 'announce') {
|
|
238
|
+
await hangupCall(dialog, endpoint, callId);
|
|
239
|
+
session.transition('COMPLETED', 'announce_complete');
|
|
240
|
+
|
|
241
|
+
} else if (mode === 'conversation') {
|
|
242
|
+
logger.info('Entering conversation mode', {
|
|
243
|
+
callId: callId,
|
|
244
|
+
device: deviceConfig ? deviceConfig.name : 'default',
|
|
245
|
+
hasContext: !!context
|
|
246
|
+
});
|
|
247
|
+
session.transition('CONVERSING');
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await runConversationLoop(endpoint, dialog, callId, {
|
|
251
|
+
audioForkServer: audioForkServer,
|
|
252
|
+
whisperClient: whisperClient,
|
|
253
|
+
claudeBridge: claudeBridge,
|
|
254
|
+
ttsService: ttsService,
|
|
255
|
+
wsPort: wsPort,
|
|
256
|
+
deviceConfig: deviceConfig,
|
|
257
|
+
initialContext: message,
|
|
258
|
+
context: context, // NEW: pass structured context
|
|
259
|
+
skipGreeting: true,
|
|
260
|
+
maxTurns: 20,
|
|
261
|
+
// Record every exchange on the session so the caller (and the MCP
|
|
262
|
+
// tool) can read the user's answer afterwards via GET /api/call/:id.
|
|
263
|
+
onTurn: function (userText, assistantText) {
|
|
264
|
+
session.recordTurn(userText, assistantText);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await hangupCall(dialog, endpoint, callId);
|
|
269
|
+
session.transition('COMPLETED', 'conversation_complete');
|
|
270
|
+
|
|
271
|
+
} catch (convError) {
|
|
272
|
+
logger.error('Conversation loop error', {
|
|
273
|
+
callId: callId,
|
|
274
|
+
error: convError.message
|
|
275
|
+
});
|
|
276
|
+
await hangupCall(dialog, endpoint, callId);
|
|
277
|
+
session.transition('COMPLETED', 'conversation_error');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger.error('Outbound call failed', {
|
|
283
|
+
callId: callId,
|
|
284
|
+
error: error.message,
|
|
285
|
+
elapsed: Date.now() - startTime
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
var reason = 'error';
|
|
289
|
+
if (error.message === 'busy') reason = 'busy';
|
|
290
|
+
else if (error.message === 'no_answer') reason = 'no_answer';
|
|
291
|
+
else if (error.message === 'not_found') reason = 'not_found';
|
|
292
|
+
else if (error.message === 'service_unavailable') reason = 'service_unavailable';
|
|
293
|
+
|
|
294
|
+
session.transition('FAILED', reason);
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.error('Outbound call endpoint error', {
|
|
300
|
+
error: error.message,
|
|
301
|
+
stack: error.stack
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
res.status(500).json({
|
|
305
|
+
success: false,
|
|
306
|
+
error: 'internal_error',
|
|
307
|
+
message: 'An internal error occurred'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* GET /api/call/:callId
|
|
314
|
+
*/
|
|
315
|
+
router.get('/call/:callId', function(req, res) {
|
|
316
|
+
var callId = req.params.callId;
|
|
317
|
+
var session = getSession(callId);
|
|
318
|
+
|
|
319
|
+
if (!session) {
|
|
320
|
+
return res.status(404).json({
|
|
321
|
+
success: false,
|
|
322
|
+
error: 'not_found',
|
|
323
|
+
message: 'Call not found or expired'
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
res.json({
|
|
328
|
+
success: true,
|
|
329
|
+
data: session.getInfo()
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* GET /api/calls
|
|
335
|
+
*/
|
|
336
|
+
router.get('/calls', function(req, res) {
|
|
337
|
+
var sessions = getAllSessions();
|
|
338
|
+
|
|
339
|
+
res.json({
|
|
340
|
+
success: true,
|
|
341
|
+
count: sessions.length,
|
|
342
|
+
calls: sessions.map(function(s) { return s.getInfo(); })
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* POST /api/call/:callId/hangup
|
|
348
|
+
*/
|
|
349
|
+
router.post('/call/:callId/hangup', async function(req, res) {
|
|
350
|
+
var callId = req.params.callId;
|
|
351
|
+
var session = getSession(callId);
|
|
352
|
+
|
|
353
|
+
if (!session) {
|
|
354
|
+
return res.status(404).json({
|
|
355
|
+
success: false,
|
|
356
|
+
error: 'not_found',
|
|
357
|
+
message: 'Call not found or expired'
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (session.state === 'COMPLETED' || session.state === 'FAILED') {
|
|
362
|
+
return res.status(400).json({
|
|
363
|
+
success: false,
|
|
364
|
+
error: 'already_ended',
|
|
365
|
+
message: 'Call has already ended'
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
await session.hangup();
|
|
371
|
+
|
|
372
|
+
res.json({
|
|
373
|
+
success: true,
|
|
374
|
+
message: 'Call hangup initiated',
|
|
375
|
+
callId: callId
|
|
376
|
+
});
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger.error('Failed to hangup call', {
|
|
379
|
+
callId: callId,
|
|
380
|
+
error: error.message
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
res.status(500).json({
|
|
384
|
+
success: false,
|
|
385
|
+
error: 'hangup_failed',
|
|
386
|
+
message: error.message
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Setup routes with dependencies
|
|
393
|
+
*/
|
|
394
|
+
function setupRoutes(deps) {
|
|
395
|
+
srf = deps.srf;
|
|
396
|
+
mediaServer = deps.mediaServer;
|
|
397
|
+
deviceRegistry = deps.deviceRegistry || null;
|
|
398
|
+
audioForkServer = deps.audioForkServer || null;
|
|
399
|
+
whisperClient = deps.whisperClient || null;
|
|
400
|
+
claudeBridge = deps.claudeBridge || null;
|
|
401
|
+
ttsService = deps.ttsService || null;
|
|
402
|
+
wsPort = deps.wsPort || 3001;
|
|
403
|
+
|
|
404
|
+
var conversationReady = !!(audioForkServer && whisperClient && claudeBridge && ttsService);
|
|
405
|
+
|
|
406
|
+
logger.info('Outbound routes initialized', {
|
|
407
|
+
srf: !!srf,
|
|
408
|
+
mediaServer: !!mediaServer,
|
|
409
|
+
deviceRegistry: !!deviceRegistry,
|
|
410
|
+
conversationMode: conversationReady ? 'enabled' : 'disabled'
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
router: router,
|
|
416
|
+
setupRoutes: setupRoutes
|
|
417
|
+
};
|