claude-phone-local 2.1.1 → 2.1.2

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.
@@ -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
- spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
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
- spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
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}`);
@@ -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.1.1",
3
+ "version": "2.1.2",
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": {