claude-phone-local 2.1.0 → 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.
@@ -1,8 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { spawn } from 'child_process';
3
- import axios from 'axios';
4
- import { loadConfig, configExists, getDockerComposePath, getPidPath } from '../config.js';
3
+ import { loadConfig, configExists, getDockerComposePath, getPidPath, getConfigDir } from '../config.js';
5
4
  import fs from 'fs';
5
+ import path from 'path';
6
6
 
7
7
  /**
8
8
  * Logs command - Tail service logs
@@ -104,49 +104,53 @@ function tailDockerLogs(dockerComposePath) {
104
104
 
105
105
  /**
106
106
  * Tail API server logs
107
- * @param {object} config - Configuration object
107
+ * @param {object} _config - Configuration object (unused; log path is fixed)
108
108
  */
109
- function tailAPIServerLogs(config) {
110
- console.log(chalk.gray('Watching Claude API server output...\n'));
109
+ function tailAPIServerLogs(_config) {
110
+ const logPath = path.join(getConfigDir(), 'claude-api-server.log');
111
111
 
112
- // Since the server runs detached, we can't easily tail its logs
113
- // Instead, we'll monitor its health endpoint
114
- console.log(chalk.yellow('Note: API server logs are not available (runs detached)'));
115
- console.log(chalk.gray('Monitoring health endpoint instead...\n'));
112
+ if (!fs.existsSync(logPath)) {
113
+ console.log(chalk.yellow('⚠ No log file yet - the API server has not written any output'));
114
+ console.log(chalk.gray(` Expected at: ${logPath}\n`));
115
+ process.exit(1);
116
+ }
117
+
118
+ console.log(chalk.gray(`Watching ${logPath}\n`));
119
+
120
+ // Print the last ~50 lines, then follow new writes. No native `tail -f` on
121
+ // Windows, so poll the file size and read only the appended bytes.
122
+ const fullContent = fs.readFileSync(logPath, 'utf8');
123
+ const lines = fullContent.split('\n');
124
+ const tailLines = lines.slice(Math.max(0, lines.length - 50));
125
+ process.stdout.write(tailLines.join('\n'));
116
126
 
117
- let consecutiveFailures = 0;
127
+ let position = fs.statSync(logPath).size;
118
128
 
119
- const checkHealth = async () => {
129
+ const poll = setInterval(() => {
120
130
  try {
121
- const response = await axios.get(`http://localhost:${config.server.claudeApiPort}/health`, {
122
- timeout: 3000
123
- });
124
-
125
- if (response.status === 200) {
126
- console.log(chalk.green(`[${new Date().toISOString()}] ✓ API server healthy`));
127
- consecutiveFailures = 0;
128
- } else {
129
- console.log(chalk.yellow(`[${new Date().toISOString()}] ⚠ Unexpected status: ${response.status}`));
131
+ const { size } = fs.statSync(logPath);
132
+ if (size < position) {
133
+ // Log file was rotated/truncated (e.g. by a restart) - start over.
134
+ position = 0;
130
135
  }
131
- } catch (error) {
132
- consecutiveFailures++;
133
- console.log(chalk.red(`[${new Date().toISOString()}] Health check failed: ${error.message}`));
134
-
135
- if (consecutiveFailures >= 3) {
136
- console.log(chalk.red('\n✗ API server appears to be down. Stopping health checks.\n'));
137
- process.exit(1);
136
+ if (size > position) {
137
+ const fd = fs.openSync(logPath, 'r');
138
+ const buffer = Buffer.alloc(size - position);
139
+ fs.readSync(fd, buffer, 0, buffer.length, position);
140
+ fs.closeSync(fd);
141
+ process.stdout.write(buffer.toString('utf8'));
142
+ position = size;
138
143
  }
144
+ } catch (err) {
145
+ console.log(chalk.red(`\n✗ Lost access to log file: ${err.message}\n`));
146
+ clearInterval(poll);
147
+ process.exit(1);
139
148
  }
140
- };
149
+ }, 1000);
141
150
 
142
- // Check immediately, then every 5 seconds
143
- checkHealth();
144
- const interval = setInterval(checkHealth, 5000);
145
-
146
- // Handle Ctrl+C
147
151
  process.on('SIGINT', () => {
148
- clearInterval(interval);
149
- console.log(chalk.gray('\n\nStopped monitoring API server.\n'));
152
+ clearInterval(poll);
153
+ console.log(chalk.gray('\n\nStopped tailing logs.\n'));
150
154
  process.exit(0);
151
155
  });
152
156
  }
@@ -157,7 +161,7 @@ function tailAPIServerLogs(config) {
157
161
  * @param {object} _config - Configuration object (unused)
158
162
  */
159
163
  function tailBothServices(dockerComposePath, _config) {
160
- console.log(chalk.gray('Showing Docker container logs (API server logs not available)\n'));
164
+ console.log(chalk.gray('Showing Docker container logs. Run "claude-phone logs api-server" separately for the host-side API server log.\n'));
161
165
 
162
166
  const child = spawn('docker', [
163
167
  'compose',
@@ -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
@@ -70,17 +70,26 @@ export async function startServer(serverPath, port, pidPath = null) {
70
70
  }
71
71
 
72
72
  return new Promise((resolve, reject) => {
73
+ // Redirect stdout/stderr to a log file instead of discarding them - this
74
+ // process's own console.log is the only place query timing, session
75
+ // tracking, and CLI/SDK errors are visible; with stdio: 'ignore' there
76
+ // was no way to diagnose a slow or failed call after the fact.
77
+ const logPath = path.join(getConfigDir(), 'claude-api-server.log');
78
+ const logFd = fs.openSync(logPath, 'a');
79
+
73
80
  // Spawn detached process
74
81
  const child = spawn('node', ['server.js'], {
75
82
  cwd: serverPath,
76
83
  detached: true,
77
- stdio: 'ignore',
84
+ stdio: ['ignore', logFd, logFd],
78
85
  env: {
79
86
  ...process.env,
80
87
  PORT: port
81
88
  }
82
89
  });
83
90
 
91
+ fs.closeSync(logFd); // child holds its own reference to the fd via dup()
92
+
84
93
  // Don't wait for child process
85
94
  child.unref();
86
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-phone-local",
3
- "version": "2.1.0",
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": {