claude-phone-local 2.0.7 → 2.0.9
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/claude-api-server/server.js +1 -1
- package/cli/lib/commands/setup.js +14 -3
- package/cli/lib/commands/start.js +22 -10
- package/cli/lib/network.js +39 -0
- package/package.json +1 -1
- package/voice-app/index.js +1 -1
- package/voice-app/lib/http-server.js +10 -2
|
@@ -118,7 +118,7 @@ console.log('[STARTUP] API keys loaded:', apiKeys.join(', '));
|
|
|
118
118
|
const sessions = new Map();
|
|
119
119
|
|
|
120
120
|
// Model selection - Sonnet for balanced speed/quality
|
|
121
|
-
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-
|
|
121
|
+
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-5';
|
|
122
122
|
|
|
123
123
|
function parseClaudeStdout(stdout) {
|
|
124
124
|
// Claude Code CLI may output JSONL; when it does, extract the `result` message.
|
|
@@ -382,8 +382,19 @@ async function setupVoiceServer(config) {
|
|
|
382
382
|
// Step 5: Server Configuration (IP only, no API port)
|
|
383
383
|
console.log(chalk.bold('\n⚙️ Server Configuration'));
|
|
384
384
|
const localIp = getLocalIP();
|
|
385
|
-
const
|
|
385
|
+
const { ipMode } = await inquirer.prompt([
|
|
386
386
|
{
|
|
387
|
+
type: 'confirm',
|
|
388
|
+
name: 'ipMode',
|
|
389
|
+
message: `Auto-detect the LAN IP on every "claude-phone start" (currently ${localIp})? ` +
|
|
390
|
+
'Recommended if this machine moves networks (laptop, DHCP) - answering ' +
|
|
391
|
+
'no locks in a fixed IP that goes stale the next time it changes.',
|
|
392
|
+
default: config.server.externalIp === 'auto' || config.server.externalIp === undefined
|
|
393
|
+
}
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
const serverAnswers = await inquirer.prompt([
|
|
397
|
+
...(ipMode ? [] : [{
|
|
387
398
|
type: 'input',
|
|
388
399
|
name: 'externalIp',
|
|
389
400
|
message: 'Server LAN IP (for RTP audio):',
|
|
@@ -397,7 +408,7 @@ async function setupVoiceServer(config) {
|
|
|
397
408
|
}
|
|
398
409
|
return true;
|
|
399
410
|
}
|
|
400
|
-
},
|
|
411
|
+
}]),
|
|
401
412
|
{
|
|
402
413
|
type: 'input',
|
|
403
414
|
name: 'httpPort',
|
|
@@ -413,7 +424,7 @@ async function setupVoiceServer(config) {
|
|
|
413
424
|
}
|
|
414
425
|
]);
|
|
415
426
|
|
|
416
|
-
config.server.externalIp = serverAnswers.externalIp;
|
|
427
|
+
config.server.externalIp = ipMode ? 'auto' : serverAnswers.externalIp;
|
|
417
428
|
config.server.httpPort = parseInt(serverAnswers.httpPort, 10);
|
|
418
429
|
|
|
419
430
|
return config;
|
|
@@ -5,8 +5,8 @@ import path from 'path';
|
|
|
5
5
|
import { loadConfig, configExists, getInstallationType } from '../config.js';
|
|
6
6
|
import { checkDocker, writeDockerConfig, startContainers } from '../docker.js';
|
|
7
7
|
import { startServer, isServerRunning } from '../process-manager.js';
|
|
8
|
-
import { isClaudeInstalled
|
|
9
|
-
import { checkClaudeApiServer } from '../network.js';
|
|
8
|
+
import { isClaudeInstalled } from '../utils.js';
|
|
9
|
+
import { checkClaudeApiServer, waitForVoiceAppReady } from '../network.js';
|
|
10
10
|
import { runPrereqChecks } from '../prereqs.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -184,10 +184,16 @@ async function startVoiceServer(config, isPiMode) {
|
|
|
184
184
|
throw error;
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
// Wait
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
spinner.
|
|
187
|
+
// Wait for FreeSWITCH to actually be ready to accept calls, not just for
|
|
188
|
+
// the container process to be up. A call landing in that gap gets a SIP
|
|
189
|
+
// 503 and falls through to 3CX's generic voicemail prompt.
|
|
190
|
+
spinner.start('Waiting for voice services to be ready...');
|
|
191
|
+
const readiness = await waitForVoiceAppReady(`http://localhost:${config.server.httpPort}`);
|
|
192
|
+
if (readiness.ready) {
|
|
193
|
+
spinner.succeed('Voice services ready');
|
|
194
|
+
} else {
|
|
195
|
+
spinner.warn('Voice services did not report ready in time - calls may briefly fail while it finishes starting');
|
|
196
|
+
}
|
|
191
197
|
|
|
192
198
|
// Success
|
|
193
199
|
console.log(chalk.bold.green('\n✓ Voice server running!\n'));
|
|
@@ -307,10 +313,16 @@ async function startBoth(config, isPiMode) {
|
|
|
307
313
|
throw error;
|
|
308
314
|
}
|
|
309
315
|
|
|
310
|
-
// Wait
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
spinner.
|
|
316
|
+
// Wait for FreeSWITCH to actually be ready to accept calls, not just for
|
|
317
|
+
// the container process to be up. A call landing in that gap gets a SIP
|
|
318
|
+
// 503 and falls through to 3CX's generic voicemail prompt.
|
|
319
|
+
spinner.start('Waiting for voice services to be ready...');
|
|
320
|
+
const readiness = await waitForVoiceAppReady(`http://localhost:${config.server.httpPort}`);
|
|
321
|
+
if (readiness.ready) {
|
|
322
|
+
spinner.succeed('Voice services ready');
|
|
323
|
+
} else {
|
|
324
|
+
spinner.warn('Voice services did not report ready in time - calls may briefly fail while it finishes starting');
|
|
325
|
+
}
|
|
314
326
|
|
|
315
327
|
// Start claude-api-server (only in standard mode - Pi mode uses remote API server)
|
|
316
328
|
if (!isPiMode) {
|
package/cli/lib/network.js
CHANGED
|
@@ -107,3 +107,42 @@ export async function checkClaudeApiServer(url) {
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Poll voice-app's /health until it reports drachtio + FreeSWITCH both
|
|
113
|
+
* connected, or the timeout elapses. Fixes `claude-phone start` reporting
|
|
114
|
+
* "All services running!" while FreeSWITCH is still booting - a call landing
|
|
115
|
+
* in that gap gets a SIP 503 and falls through to the 3CX generic voicemail
|
|
116
|
+
* prompt instead of ever reaching the app.
|
|
117
|
+
* @param {string} url - voice-app base URL (e.g. http://localhost:3000)
|
|
118
|
+
* @param {object} [opts]
|
|
119
|
+
* @param {number} [opts.timeoutMs=30000] - Give up after this long
|
|
120
|
+
* @param {number} [opts.intervalMs=1000] - Poll interval
|
|
121
|
+
* @returns {Promise<{ready: boolean, timedOut: boolean}>}
|
|
122
|
+
*/
|
|
123
|
+
export async function waitForVoiceAppReady(url, { timeoutMs = 30000, intervalMs = 1000 } = {}) {
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
const ready = await new Promise((resolve) => {
|
|
128
|
+
const req = http.get(url + '/health', { timeout: 3000 }, (res) => {
|
|
129
|
+
let body = '';
|
|
130
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
131
|
+
res.on('end', () => {
|
|
132
|
+
try {
|
|
133
|
+
resolve(Boolean(JSON.parse(body).ready));
|
|
134
|
+
} catch {
|
|
135
|
+
resolve(false);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
req.on('error', () => resolve(false));
|
|
140
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (ready) return { ready: true, timedOut: false };
|
|
144
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { ready: false, timedOut: true };
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-phone-local",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "Local/offline fork of NetworkChuck's claude-phone: talk to Claude Code over 3CX/SIP with faster-whisper STT + Piper TTS in one Docker container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/voice-app/index.js
CHANGED
|
@@ -178,7 +178,7 @@ function initializeServers() {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// HTTP server for TTS audio
|
|
181
|
-
httpServer = createHttpServer(config.audio_dir, config.http_port);
|
|
181
|
+
httpServer = createHttpServer(config.audio_dir, config.http_port, function () { return isReady; });
|
|
182
182
|
console.log("[" + new Date().toISOString() + "] HTTP Server started on port " + config.http_port);
|
|
183
183
|
|
|
184
184
|
// WebSocket server for audio fork
|
|
@@ -24,9 +24,13 @@ const FILE_MAX_AGE = 600000;
|
|
|
24
24
|
*
|
|
25
25
|
* @param {string} audioDir - Directory to serve audio files from
|
|
26
26
|
* @param {number} port - Port to listen on (default: 3000)
|
|
27
|
+
* @param {() => boolean} [getReadiness] - Returns whether drachtio + FreeSWITCH
|
|
28
|
+
* are both connected. /health reflects this instead of always reporting
|
|
29
|
+
* healthy, so `claude-phone start` can actually wait for the stack to be
|
|
30
|
+
* able to accept calls instead of guessing with a fixed sleep.
|
|
27
31
|
* @returns {Object} { app, server, saveAudio, getAudioUrl, close, finalize }
|
|
28
32
|
*/
|
|
29
|
-
function createHttpServer(audioDir, port = 3000) {
|
|
33
|
+
function createHttpServer(audioDir, port = 3000, getReadiness) {
|
|
30
34
|
const app = express();
|
|
31
35
|
|
|
32
36
|
// Parse JSON bodies
|
|
@@ -58,10 +62,14 @@ function createHttpServer(audioDir, port = 3000) {
|
|
|
58
62
|
}
|
|
59
63
|
}));
|
|
60
64
|
|
|
61
|
-
// Health check endpoint
|
|
65
|
+
// Health check endpoint. `ready` reflects whether drachtio + FreeSWITCH are
|
|
66
|
+
// both connected - the process being up and able to accept calls are not
|
|
67
|
+
// the same thing, and a caller landing in the gap gets a SIP 503.
|
|
62
68
|
app.get('/health', (req, res) => {
|
|
69
|
+
const ready = typeof getReadiness === 'function' ? getReadiness() : true;
|
|
63
70
|
res.json({
|
|
64
71
|
status: 'healthy',
|
|
72
|
+
ready,
|
|
65
73
|
timestamp: new Date().toISOString(),
|
|
66
74
|
audioDir,
|
|
67
75
|
port
|