claude-phone-local 2.1.1 → 2.2.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/claude-api-server/server.js +12 -0
- package/cli/lib/commands/start.js +23 -3
- package/cli/lib/network.js +27 -0
- package/package.json +1 -1
- package/voice-app/lib/sip-handler.js +18 -0
|
@@ -159,10 +159,22 @@ SLACK DELIVERY: When the caller requests delivery to Slack (phrases like "send t
|
|
|
159
159
|
|
|
160
160
|
The caller may hang up while you're working (they'll hear hold music). That's fine - complete the work and send to Slack. They'll see it there.
|
|
161
161
|
|
|
162
|
+
END OF CALL: The phone app matches a fixed list of goodbye words (bye, goodbye, hang up, etc.) to end the call automatically, so it misses anything phrased differently - "that's everything, thanks", "I'm all set", "nothing else for now", "we're done here". You understand intent better than a keyword match does, so ALSO add a third line whenever the caller's message signals the conversation is actually finished (a clear goodbye/thanks-and-done in any wording, in any language) - not for a mid-conversation pause, not for "give me a second", not merely because their immediate question was answered:
|
|
163
|
+
|
|
164
|
+
🔚 END_CALL: true
|
|
165
|
+
|
|
166
|
+
Omit this line entirely (do not write "false") on every turn that isn't a real goodbye - most turns won't have it.
|
|
167
|
+
|
|
162
168
|
Example query: "What's the weather in Royce City?"
|
|
163
169
|
Example response:
|
|
164
170
|
🗣️ VOICE_RESPONSE: It's 65 degrees and partly cloudy in Royce City right now. Great weather for being outside!
|
|
165
171
|
🎯 COMPLETED: Weather lookup for Royce City done.
|
|
172
|
+
|
|
173
|
+
Example query: "Great, that's everything, thanks!"
|
|
174
|
+
Example response:
|
|
175
|
+
🗣️ VOICE_RESPONSE: You're welcome! Have a great day.
|
|
176
|
+
🎯 COMPLETED: Caller signed off, no further requests.
|
|
177
|
+
🔚 END_CALL: true
|
|
166
178
|
[END VOICE CONTEXT]
|
|
167
179
|
|
|
168
180
|
`;
|
|
@@ -6,7 +6,7 @@ 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
8
|
import { isClaudeInstalled } from '../utils.js';
|
|
9
|
-
import { checkClaudeApiServer, waitForVoiceAppReady } from '../network.js';
|
|
9
|
+
import { checkClaudeApiServer, waitForVoiceAppReady, waitForClaudeApiServerReady } from '../network.js';
|
|
10
10
|
import { runPrereqChecks } from '../prereqs.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -91,7 +91,17 @@ async function startApiServer(config) {
|
|
|
91
91
|
spinner.warn('Claude API server already running');
|
|
92
92
|
} else {
|
|
93
93
|
await startServer(config.paths.claudeApiServer, config.server.claudeApiPort);
|
|
94
|
-
|
|
94
|
+
// startServer only confirms the process was spawned, not that it
|
|
95
|
+
// actually bound its port and stayed up (e.g. a stale process already
|
|
96
|
+
// on that port crashes it with EADDRINUSE within milliseconds).
|
|
97
|
+
const readiness = await waitForClaudeApiServerReady(`http://localhost:${config.server.claudeApiPort}`);
|
|
98
|
+
if (readiness.healthy) {
|
|
99
|
+
spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
|
|
100
|
+
} else {
|
|
101
|
+
spinner.fail(`Claude API server did not become healthy: ${readiness.error || 'timed out'}`);
|
|
102
|
+
console.log(chalk.yellow(`\n Check the log: claude-phone logs api-server\n`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
95
105
|
}
|
|
96
106
|
} catch (error) {
|
|
97
107
|
spinner.fail(`Failed to start server: ${error.message}`);
|
|
@@ -332,7 +342,17 @@ async function startBoth(config, isPiMode) {
|
|
|
332
342
|
spinner.warn('Claude API server already running');
|
|
333
343
|
} else {
|
|
334
344
|
await startServer(config.paths.claudeApiServer, config.server.claudeApiPort);
|
|
335
|
-
|
|
345
|
+
// startServer only confirms the process was spawned, not that it
|
|
346
|
+
// actually bound its port and stayed up (e.g. a stale process
|
|
347
|
+
// already on that port crashes it with EADDRINUSE within ms).
|
|
348
|
+
const apiReadiness = await waitForClaudeApiServerReady(`http://localhost:${config.server.claudeApiPort}`);
|
|
349
|
+
if (apiReadiness.healthy) {
|
|
350
|
+
spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
|
|
351
|
+
} else {
|
|
352
|
+
spinner.fail(`Claude API server did not become healthy: ${apiReadiness.error || 'timed out'}`);
|
|
353
|
+
console.log(chalk.yellow(`\n Check the log: claude-phone logs api-server\n`));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
336
356
|
}
|
|
337
357
|
} catch (error) {
|
|
338
358
|
spinner.fail(`Failed to start server: ${error.message}`);
|
package/cli/lib/network.js
CHANGED
|
@@ -108,6 +108,33 @@ export async function checkClaudeApiServer(url) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Poll claude-api-server's /health until it responds, or the timeout
|
|
113
|
+
* elapses. startServer() only confirms the process was spawned (got a PID) -
|
|
114
|
+
* not that it actually bound its port and stayed up. A crash right after
|
|
115
|
+
* spawn (e.g. EADDRINUSE from a stale process already on that port) was
|
|
116
|
+
* previously reported as "✔ Claude API server started" and "✓ All services
|
|
117
|
+
* running!" with no indication anything was wrong.
|
|
118
|
+
* @param {string} url - claude-api-server base URL (e.g. http://localhost:3333)
|
|
119
|
+
* @param {object} [opts]
|
|
120
|
+
* @param {number} [opts.timeoutMs=10000] - Give up after this long
|
|
121
|
+
* @param {number} [opts.intervalMs=500] - Poll interval
|
|
122
|
+
* @returns {Promise<{healthy: boolean, timedOut: boolean, error?: string}>}
|
|
123
|
+
*/
|
|
124
|
+
export async function waitForClaudeApiServerReady(url, { timeoutMs = 10000, intervalMs = 500 } = {}) {
|
|
125
|
+
const deadline = Date.now() + timeoutMs;
|
|
126
|
+
let lastError;
|
|
127
|
+
|
|
128
|
+
while (Date.now() < deadline) {
|
|
129
|
+
const result = await checkClaudeApiServer(url);
|
|
130
|
+
if (result.reachable && result.healthy) return { healthy: true, timedOut: false };
|
|
131
|
+
lastError = result.error;
|
|
132
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { healthy: false, timedOut: true, error: lastError };
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
/**
|
|
112
139
|
* Poll voice-app's /health until it reports drachtio + FreeSWITCH both
|
|
113
140
|
* connected, or the timeout elapses. Fixes `claude-phone start` reporting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-phone-local",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
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": {
|
|
@@ -162,6 +162,17 @@ function extractVoiceLine(response) {
|
|
|
162
162
|
return response.substring(0, 500).trim();
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Whether Claude's raw response signals the conversation is actually over.
|
|
167
|
+
* Complements isGoodbye()'s fixed phrase match on the caller's own words -
|
|
168
|
+
* Claude sees the full exchange and can recognize a sign-off phrased in ways
|
|
169
|
+
* the keyword list doesn't cover ("that's everything, thanks", "I'm all
|
|
170
|
+
* set"), in any language, without maintaining a growing keyword list.
|
|
171
|
+
*/
|
|
172
|
+
function hasEndCallMarker(response) {
|
|
173
|
+
return /🔚\s*END_CALL:\s*true/i.test(response);
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
/**
|
|
166
177
|
* Play a clip the caller is allowed to interrupt.
|
|
167
178
|
*
|
|
@@ -443,6 +454,13 @@ async function conversationLoop(endpoint, dialog, callUuid, options, deviceConfi
|
|
|
443
454
|
const responseBarge = await playInterruptible(endpoint, session, responseUrl);
|
|
444
455
|
if (responseBarge) {
|
|
445
456
|
pendingUtterance = responseBarge;
|
|
457
|
+
} else if (hasEndCallMarker(claudeResponse)) {
|
|
458
|
+
// Claude judged this a real sign-off from the caller's own words
|
|
459
|
+
// (not just isGoodbye()'s fixed phrase match) - she already said
|
|
460
|
+
// her goodbye as part of the normal response above, so just end
|
|
461
|
+
// the call rather than waiting for another turn.
|
|
462
|
+
console.log('[' + new Date().toISOString() + '] CONVERSATION Claude signaled end of call');
|
|
463
|
+
break;
|
|
446
464
|
}
|
|
447
465
|
}
|
|
448
466
|
|