voicecc 1.2.11 → 1.2.13

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Claude Voice</title>
7
- <script type="module" crossorigin src="/assets/index-DCeOdulF.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-DbjqXBdo.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-DeN1WDo9.css">
9
9
  </head>
10
10
  <body>
@@ -192,18 +192,18 @@ export function agentsRoutes(): Hono {
192
192
  }
193
193
 
194
194
  // Place the actual Twilio call
195
- const client = twilioSdk(accountSid, authToken);
196
- const numbers = await client.incomingPhoneNumbers.list({ limit: 1 });
197
- if (numbers.length === 0) {
198
- return c.json({ error: "No Twilio phone numbers found on this account" }, 400);
195
+ const fromNumber = envVars.TWILIO_PHONE_NUMBER;
196
+ if (!fromNumber) {
197
+ return c.json({ error: "No Twilio phone number selected. Select one in Twilio settings." }, 400);
199
198
  }
200
199
 
200
+ const client = twilioSdk(accountSid, authToken);
201
201
  const tunnelHost = tunnelUrl.replace(/^https?:\/\//, "");
202
202
  const twiml = `<Response><Connect><Stream url="wss://${tunnelHost}/media/${token}?agentId=${id}" /></Connect></Response>`;
203
203
 
204
204
  const call = await client.calls.create({
205
205
  to: userPhone,
206
- from: numbers[0].phoneNumber,
206
+ from: fromNumber,
207
207
  twiml,
208
208
  });
209
209
 
@@ -134,17 +134,15 @@ export function twilioRoutes(): Hono {
134
134
  }
135
135
 
136
136
  try {
137
- const client = twilioSdk(accountSid, authToken);
138
-
139
- // Get the first Twilio phone number to use as caller ID
140
- const numbers = await client.incomingPhoneNumbers.list({ limit: 1 });
141
- if (numbers.length === 0) {
142
- return c.json({ error: "No Twilio phone numbers found on this account" }, 400);
137
+ const fromNumber = envVars.TWILIO_PHONE_NUMBER;
138
+ if (!fromNumber) {
139
+ return c.json({ error: "No Twilio phone number selected. Select one in step 2." }, 400);
143
140
  }
144
141
 
142
+ const client = twilioSdk(accountSid, authToken);
145
143
  const call = await client.calls.create({
146
144
  to,
147
- from: numbers[0].phoneNumber,
145
+ from: fromNumber,
148
146
  twiml: '<Response><Say>This is a test call from your voice assistant. If you can hear this, your Twilio setup is working correctly. Goodbye!</Say></Response>',
149
147
  });
150
148
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicecc",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "Voice Agent Platform running on Claude Code -- create and deploy conversational voice agents with ElevenLabs STT/TTS and VAD",
5
5
  "repository": {
6
6
  "type": "git",
package/server/index.ts CHANGED
@@ -43,6 +43,18 @@ const VOICE_SERVER_DIR = join(import.meta.dirname ?? ".", "..", "voice-server");
43
43
  /** Reference to the Python voice server child process */
44
44
  let pythonProcess: ChildProcess | null = null;
45
45
 
46
+ /** Maximum number of automatic restart attempts after an unexpected crash */
47
+ const PYTHON_MAX_RESTART_ATTEMPTS = 5;
48
+
49
+ /** Delay before attempting a restart (ms) */
50
+ const PYTHON_RESTART_DELAY_MS = 3_000;
51
+
52
+ /** Number of consecutive restart attempts since last successful start */
53
+ let pythonRestartAttempts = 0;
54
+
55
+ /** Whether the Python server was intentionally stopped (skip auto-restart) */
56
+ let pythonManuallyStopped = false;
57
+
46
58
  /**
47
59
  * Start the Python voice server as a child process.
48
60
  * Waits for the health endpoint to respond before returning.
@@ -63,6 +75,7 @@ async function startPythonVoiceServer(): Promise<void> {
63
75
  pythonProcess.on("exit", (code) => {
64
76
  console.error(`Python voice server exited with code ${code}`);
65
77
  pythonProcess = null;
78
+ schedulePythonRestart();
66
79
  });
67
80
 
68
81
  // Wait for health endpoint (up to 15s)
@@ -72,6 +85,7 @@ async function startPythonVoiceServer(): Promise<void> {
72
85
  const res = await fetch(`${VOICE_SERVER_API_URL}/health`);
73
86
  if (res.ok) {
74
87
  console.log("Python voice server is ready");
88
+ pythonRestartAttempts = 0;
75
89
  return;
76
90
  }
77
91
  } catch {
@@ -83,15 +97,67 @@ async function startPythonVoiceServer(): Promise<void> {
83
97
  }
84
98
 
85
99
  /**
86
- * Stop the Python voice server child process.
100
+ * Stop the Python voice server child process. Prevents auto-restart.
87
101
  */
88
102
  function stopPythonVoiceServer(): void {
103
+ pythonManuallyStopped = true;
89
104
  if (pythonProcess) {
90
105
  pythonProcess.kill("SIGTERM");
91
106
  pythonProcess = null;
92
107
  }
93
108
  }
94
109
 
110
+ /**
111
+ * Schedule an automatic restart of the Python voice server after an unexpected exit.
112
+ * Skips restart if manually stopped or max attempts exceeded.
113
+ */
114
+ function schedulePythonRestart(): void {
115
+ if (pythonManuallyStopped) {
116
+ return;
117
+ }
118
+
119
+ pythonRestartAttempts++;
120
+
121
+ if (pythonRestartAttempts > PYTHON_MAX_RESTART_ATTEMPTS) {
122
+ console.error(`[voice-server] Giving up after ${PYTHON_MAX_RESTART_ATTEMPTS} restart attempts`);
123
+ return;
124
+ }
125
+
126
+ console.log(
127
+ `[voice-server] Restarting in ${PYTHON_RESTART_DELAY_MS / 1000}s ` +
128
+ `(attempt ${pythonRestartAttempts}/${PYTHON_MAX_RESTART_ATTEMPTS})...`
129
+ );
130
+
131
+ setTimeout(async () => {
132
+ if (pythonManuallyStopped || pythonProcess) {
133
+ return;
134
+ }
135
+
136
+ try {
137
+ await startPythonVoiceServer();
138
+ pythonRestartAttempts = 0;
139
+ console.log("[voice-server] Restarted successfully");
140
+
141
+ // Re-notify of tunnel URL if tunnel is running
142
+ const currentTunnelUrl = getTunnelUrl();
143
+ if (currentTunnelUrl) {
144
+ try {
145
+ await fetch(`${VOICE_SERVER_API_URL}/config/tunnel-url`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ url: currentTunnelUrl }),
149
+ });
150
+ } catch {
151
+ console.warn("[voice-server] Failed to re-notify tunnel URL after restart");
152
+ }
153
+ }
154
+ } catch (err) {
155
+ console.error(`[voice-server] Restart failed: ${err}`);
156
+ schedulePythonRestart();
157
+ }
158
+ }, PYTHON_RESTART_DELAY_MS);
159
+ }
160
+
95
161
  // Use VOICECC_DIR env var if set (passed by CLI when dropping root privileges),
96
162
  // otherwise fall back to ~/.voicecc.
97
163
  const VOICECC_DIR = process.env.VOICECC_DIR ?? join(homedir(), ".voicecc");
@@ -70,26 +70,36 @@ export async function startTwilioServer(_dashboardPort: number, tunnelUrl?: stri
70
70
  throw new Error("TWILIO_AUTH_TOKEN is not set in .env");
71
71
  }
72
72
 
73
+ if (!envVars.TWILIO_PHONE_NUMBER) {
74
+ throw new Error("TWILIO_PHONE_NUMBER is not set in .env");
75
+ }
76
+
73
77
  const accountSid = envVars.TWILIO_ACCOUNT_SID;
74
78
  const webhookUrl = tunnelUrl ? `${tunnelUrl}/api/twilio/incoming-call` : null;
75
79
 
76
80
  if (tunnelUrl && accountSid && envVars.TWILIO_AUTH_TOKEN) {
77
81
  const client = twilioSdk(accountSid, envVars.TWILIO_AUTH_TOKEN);
78
82
 
79
- // Update all phone numbers on the account to point to the new webhook URL
83
+ // Update the selected phone number's webhook URL
84
+ const selectedNumber = envVars.TWILIO_PHONE_NUMBER;
80
85
  try {
81
- const numbers = await client.incomingPhoneNumbers.list();
82
- for (const num of numbers) {
83
- await client.incomingPhoneNumbers(num.sid).update({
84
- voiceUrl: webhookUrl!,
85
- voiceMethod: "POST",
86
- });
87
- }
88
- if (numbers.length > 0) {
89
- console.log(`Updated ${numbers.length} phone number(s) webhook to ${webhookUrl}`);
86
+ if (selectedNumber) {
87
+ // Find the SID for the selected number and update only that one
88
+ const numbers = await client.incomingPhoneNumbers.list({ phoneNumber: selectedNumber });
89
+ if (numbers.length > 0) {
90
+ await client.incomingPhoneNumbers(numbers[0].sid).update({
91
+ voiceUrl: webhookUrl!,
92
+ voiceMethod: "POST",
93
+ });
94
+ console.log(`Updated webhook for ${selectedNumber} to ${webhookUrl}`);
95
+ } else {
96
+ console.error(`Selected phone number ${selectedNumber} not found on Twilio account`);
97
+ }
98
+ } else {
99
+ console.warn("No TWILIO_PHONE_NUMBER configured, skipping webhook setup");
90
100
  }
91
101
  } catch (err) {
92
- console.error(`Failed to update phone number webhooks: ${err}`);
102
+ console.error(`Failed to update phone number webhook: ${err}`);
93
103
  }
94
104
  }
95
105
 
@@ -89,6 +89,7 @@ class VoiceServerConfig:
89
89
  project_root: str
90
90
  twilio_account_sid: str
91
91
  twilio_auth_token: str
92
+ twilio_phone_number: str
92
93
  user_phone_number: str
93
94
  max_concurrent_sessions: int
94
95
 
@@ -127,6 +128,7 @@ def load_config() -> VoiceServerConfig:
127
128
  project_root=PROJECT_ROOT,
128
129
  twilio_account_sid=os.environ.get("TWILIO_ACCOUNT_SID", ""),
129
130
  twilio_auth_token=os.environ.get("TWILIO_AUTH_TOKEN", ""),
131
+ twilio_phone_number=os.environ.get("TWILIO_PHONE_NUMBER", ""),
130
132
  user_phone_number=os.environ.get("USER_PHONE_NUMBER", ""),
131
133
  max_concurrent_sessions=int(
132
134
  os.environ.get("MAX_CONCURRENT_SESSIONS") or DEFAULT_MAX_CONCURRENT_SESSIONS
@@ -17,6 +17,7 @@ Responsibilities:
17
17
  """
18
18
 
19
19
  import asyncio
20
+ import gc
20
21
  import json
21
22
  import logging
22
23
  import os
@@ -289,6 +290,9 @@ async def check_single_agent(agent: Agent) -> HeartbeatResult:
289
290
  pass
290
291
  _in_flight_checks.discard(agent.id)
291
292
 
293
+ # Force garbage collection to reclaim memory from the Claude subprocess
294
+ gc.collect()
295
+
292
296
 
293
297
  async def _run_heartbeat_session(
294
298
  agent: Agent, timeout_s: float
@@ -410,14 +414,14 @@ async def initiate_agent_call(agent: Agent, client: ClaudeSDKClient) -> str:
410
414
  # Schedule cleanup for unanswered calls
411
415
  asyncio.create_task(_cleanup_pending_call(token))
412
416
 
413
- # Get from number via Twilio API
417
+ # Get the configured Twilio phone number
414
418
  from twilio.rest import Client as TwilioClient
415
419
 
420
+ from_number: str = _config.twilio_phone_number
421
+ if not from_number:
422
+ raise RuntimeError("No TWILIO_PHONE_NUMBER configured")
423
+
416
424
  twilio_client = TwilioClient(_config.twilio_account_sid, _config.twilio_auth_token)
417
- numbers = twilio_client.incoming_phone_numbers.list(limit=1)
418
- if not numbers:
419
- raise RuntimeError("No Twilio phone numbers found on the account")
420
- from_number: str = numbers[0].phone_number or ""
421
425
 
422
426
  # Build TwiML with WebSocket stream URL
423
427
  tunnel_host = tunnel_url.replace("https://", "").replace("http://", "")