voicecc 1.1.14 → 1.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicecc",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "Voice mode plugin for Claude Code -- hands-free interaction via ElevenLabs STT/TTS and VAD",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * Responsibilities:
8
8
  * - Start a quick tunnel on a given port and capture the public HTTPS URL
9
+ * - Auto-restart the tunnel on unexpected crashes (up to MAX_RESTART_ATTEMPTS)
10
+ * - Log connection-level events (connected/disconnected) for observability
9
11
  * - Stop the tunnel and clear state
10
12
  * - Expose tunnel state (URL, running status, start time)
11
13
  */
@@ -18,7 +20,13 @@ import { writeEnvKey } from "./env.js";
18
20
  // ============================================================================
19
21
 
20
22
  /** Timeout for waiting for the tunnel URL to appear */
21
- const TUNNEL_URL_TIMEOUT_MS = 30000;
23
+ const TUNNEL_URL_TIMEOUT_MS = 15_000;
24
+
25
+ /** Maximum number of automatic restart attempts after an unexpected crash */
26
+ const MAX_RESTART_ATTEMPTS = 5;
27
+
28
+ /** Delay before attempting a restart (ms) */
29
+ const RESTART_DELAY_MS = 3_000;
22
30
 
23
31
  // ============================================================================
24
32
  // STATE
@@ -33,6 +41,15 @@ let tunnelUrl: string | null = null;
33
41
  /** Timestamp when tunnel URL was obtained */
34
42
  let tunnelStartedAt: number | null = null;
35
43
 
44
+ /** Port used for the current tunnel (needed for auto-restart) */
45
+ let tunnelPort: number | null = null;
46
+
47
+ /** Number of consecutive restart attempts since last successful start */
48
+ let restartAttempts = 0;
49
+
50
+ /** Whether the tunnel was intentionally stopped (skip auto-restart) */
51
+ let manuallyStopped = false;
52
+
36
53
  // ============================================================================
37
54
  // MAIN HANDLERS
38
55
  // ============================================================================
@@ -50,6 +67,70 @@ export async function startTunnel(port: number): Promise<string> {
50
67
  throw new Error("Tunnel is already running");
51
68
  }
52
69
 
70
+ manuallyStopped = false;
71
+ tunnelPort = port;
72
+
73
+ const url = await createTunnel(port);
74
+
75
+ // Reset restart counter on successful start
76
+ restartAttempts = 0;
77
+
78
+ tunnelUrl = url;
79
+ tunnelStartedAt = Date.now();
80
+ await writeEnvKey("TWILIO_WEBHOOK_URL", url);
81
+ console.log(`Tunnel URL: ${url}`);
82
+ return url;
83
+ }
84
+
85
+ /**
86
+ * Stop the tunnel and clear state. Prevents auto-restart.
87
+ */
88
+ export function stopTunnel(): void {
89
+ manuallyStopped = true;
90
+ if (activeTunnel) {
91
+ activeTunnel.stop();
92
+ }
93
+ clearTunnelState();
94
+ }
95
+
96
+ /**
97
+ * Return the current public tunnel URL, or null if not running.
98
+ *
99
+ * @returns The public HTTPS URL or null
100
+ */
101
+ export function getTunnelUrl(): string | null {
102
+ return tunnelUrl;
103
+ }
104
+
105
+ /**
106
+ * Return the timestamp when the tunnel URL was obtained, or null.
107
+ *
108
+ * @returns Unix ms timestamp or null
109
+ */
110
+ export function getTunnelStartedAt(): number | null {
111
+ return tunnelStartedAt;
112
+ }
113
+
114
+ /**
115
+ * Check whether the tunnel process is currently alive.
116
+ *
117
+ * @returns True if tunnel is running
118
+ */
119
+ export function isTunnelRunning(): boolean {
120
+ return activeTunnel !== null;
121
+ }
122
+
123
+ // ============================================================================
124
+ // HELPER FUNCTIONS
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Create a new tunnel instance, wait for its URL, and attach event handlers.
129
+ *
130
+ * @param port - Local port to tunnel
131
+ * @returns The public HTTPS URL
132
+ */
133
+ async function createTunnel(port: number): Promise<string> {
53
134
  const tunnel = Tunnel.quick(`http://localhost:${port}`);
54
135
  activeTunnel = tunnel;
55
136
 
@@ -61,15 +142,24 @@ export async function startTunnel(port: number): Promise<string> {
61
142
  console.log(`[cloudflared] ${data.trim()}`);
62
143
  });
63
144
 
145
+ // Log connection-level events for observability
146
+ tunnel.on("connected", (conn: { id: string; ip: string; location: string }) => {
147
+ console.log(`[cloudflared] Connected: ${conn.location} (${conn.ip})`);
148
+ });
149
+ tunnel.on("disconnected", (conn: { id: string; ip: string; location: string }) => {
150
+ console.log(`[cloudflared] Disconnected: ${conn.location} (${conn.ip})`);
151
+ });
152
+
153
+ // Wait for the tunnel URL or fail
64
154
  const url = await new Promise<string>((resolve, reject) => {
65
155
  const timeout = setTimeout(() => {
66
156
  activeTunnel = null;
67
- reject(new Error("Timed out waiting for tunnel URL (30s)"));
157
+ reject(new Error("Timed out waiting for tunnel URL (15s)"));
68
158
  }, TUNNEL_URL_TIMEOUT_MS);
69
159
 
70
- tunnel.once("url", (url: string) => {
160
+ tunnel.once("url", (emittedUrl: string) => {
71
161
  clearTimeout(timeout);
72
- resolve(url);
162
+ resolve(emittedUrl);
73
163
  });
74
164
 
75
165
  tunnel.once("error", (err: Error) => {
@@ -85,54 +175,64 @@ export async function startTunnel(port: number): Promise<string> {
85
175
  });
86
176
  });
87
177
 
178
+ // After successful start, handle unexpected exit with auto-restart
88
179
  tunnel.on("exit", () => {
89
- console.log("cloudflared exited");
90
- activeTunnel = null;
91
- tunnelUrl = null;
180
+ console.log("cloudflared exited unexpectedly");
181
+ clearTunnelState();
182
+ scheduleRestart();
92
183
  });
93
184
 
94
- tunnelUrl = url;
95
- tunnelStartedAt = Date.now();
96
- await writeEnvKey("TWILIO_WEBHOOK_URL", url);
97
- console.log(`Tunnel URL: ${url}`);
98
185
  return url;
99
186
  }
100
187
 
101
188
  /**
102
- * Stop the tunnel and clear state.
189
+ * Clear all tunnel state variables.
103
190
  */
104
- export function stopTunnel(): void {
105
- if (activeTunnel) {
106
- activeTunnel.stop();
107
- }
191
+ function clearTunnelState(): void {
108
192
  activeTunnel = null;
109
193
  tunnelUrl = null;
110
194
  tunnelStartedAt = null;
111
195
  }
112
196
 
113
197
  /**
114
- * Return the current public tunnel URL, or null if not running.
115
- *
116
- * @returns The public HTTPS URL or null
198
+ * Schedule an automatic restart if the tunnel crashed unexpectedly.
199
+ * Skips restart if the tunnel was manually stopped or max attempts exceeded.
117
200
  */
118
- export function getTunnelUrl(): string | null {
119
- return tunnelUrl;
120
- }
201
+ function scheduleRestart(): void {
202
+ if (manuallyStopped) {
203
+ return;
204
+ }
121
205
 
122
- /**
123
- * Return the timestamp when the tunnel URL was obtained, or null.
124
- *
125
- * @returns Unix ms timestamp or null
126
- */
127
- export function getTunnelStartedAt(): number | null {
128
- return tunnelStartedAt;
129
- }
206
+ if (tunnelPort === null) {
207
+ console.error("[tunnel] Cannot restart: no port configured");
208
+ return;
209
+ }
130
210
 
131
- /**
132
- * Check whether the tunnel process is currently alive.
133
- *
134
- * @returns True if tunnel is running
135
- */
136
- export function isTunnelRunning(): boolean {
137
- return activeTunnel !== null;
211
+ restartAttempts++;
212
+
213
+ if (restartAttempts > MAX_RESTART_ATTEMPTS) {
214
+ console.error(`[tunnel] Giving up after ${MAX_RESTART_ATTEMPTS} restart attempts`);
215
+ return;
216
+ }
217
+
218
+ const port = tunnelPort;
219
+ console.log(`[tunnel] Restarting in ${RESTART_DELAY_MS / 1000}s (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
220
+
221
+ setTimeout(async () => {
222
+ if (manuallyStopped || activeTunnel) {
223
+ return;
224
+ }
225
+
226
+ try {
227
+ const url = await createTunnel(port);
228
+ restartAttempts = 0;
229
+ tunnelUrl = url;
230
+ tunnelStartedAt = Date.now();
231
+ await writeEnvKey("TWILIO_WEBHOOK_URL", url);
232
+ console.log(`[tunnel] Restarted successfully: ${url}`);
233
+ } catch (err) {
234
+ console.error(`[tunnel] Restart failed: ${err}`);
235
+ scheduleRestart();
236
+ }
237
+ }, RESTART_DELAY_MS);
138
238
  }