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.
- package/cli/lib/commands/logs.js +40 -36
- package/cli/lib/commands/start.js +23 -3
- package/cli/lib/network.js +27 -0
- package/cli/lib/process-manager.js +10 -1
- package/package.json +1 -1
package/cli/lib/commands/logs.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
|
-
import
|
|
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}
|
|
107
|
+
* @param {object} _config - Configuration object (unused; log path is fixed)
|
|
108
108
|
*/
|
|
109
|
-
function tailAPIServerLogs(
|
|
110
|
-
|
|
109
|
+
function tailAPIServerLogs(_config) {
|
|
110
|
+
const logPath = path.join(getConfigDir(), 'claude-api-server.log');
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
127
|
+
let position = fs.statSync(logPath).size;
|
|
118
128
|
|
|
119
|
-
const
|
|
129
|
+
const poll = setInterval(() => {
|
|
120
130
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
149
|
-
console.log(chalk.gray('\n\nStopped
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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": {
|