claude-phone-local 2.0.0
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/.env.example +108 -0
- package/Dockerfile +84 -0
- package/LICENSE +22 -0
- package/README.md +231 -0
- package/claude-api-server/package-lock.json +832 -0
- package/claude-api-server/package.json +20 -0
- package/claude-api-server/server.js +540 -0
- package/claude-api-server/structured.js +208 -0
- package/cli/README.md +231 -0
- package/cli/bin/check-publish-safety.js +53 -0
- package/cli/bin/claude-phone.js +23 -0
- package/cli/bin/cli-main.js +284 -0
- package/cli/bin/postinstall.js +20 -0
- package/cli/lib/commands/api-server.js +77 -0
- package/cli/lib/commands/backup.js +69 -0
- package/cli/lib/commands/config/path.js +30 -0
- package/cli/lib/commands/config/reset.js +73 -0
- package/cli/lib/commands/config/show.js +86 -0
- package/cli/lib/commands/device/add.js +143 -0
- package/cli/lib/commands/device/list.js +55 -0
- package/cli/lib/commands/device/remove.js +83 -0
- package/cli/lib/commands/doctor.js +422 -0
- package/cli/lib/commands/logs.js +186 -0
- package/cli/lib/commands/restore.js +172 -0
- package/cli/lib/commands/setup.js +1246 -0
- package/cli/lib/commands/start.js +346 -0
- package/cli/lib/commands/status.js +139 -0
- package/cli/lib/commands/stop.js +101 -0
- package/cli/lib/commands/uninstall.js +183 -0
- package/cli/lib/commands/update.js +205 -0
- package/cli/lib/config.js +113 -0
- package/cli/lib/docker.js +384 -0
- package/cli/lib/mcp-register.js +108 -0
- package/cli/lib/network.js +109 -0
- package/cli/lib/platform.js +85 -0
- package/cli/lib/port-check.js +135 -0
- package/cli/lib/prereqs/checks/compose.js +147 -0
- package/cli/lib/prereqs/checks/disk.js +89 -0
- package/cli/lib/prereqs/checks/docker.js +155 -0
- package/cli/lib/prereqs/checks/network.js +78 -0
- package/cli/lib/prereqs/checks/node.js +95 -0
- package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
- package/cli/lib/prereqs/installers/docker.js +229 -0
- package/cli/lib/prereqs/installers/node.js +254 -0
- package/cli/lib/prereqs/platform.js +175 -0
- package/cli/lib/prereqs/utils/execute.js +208 -0
- package/cli/lib/prereqs/utils/rollback.js +223 -0
- package/cli/lib/prereqs/utils/sudo.js +177 -0
- package/cli/lib/prereqs.js +228 -0
- package/cli/lib/prerequisites.js +115 -0
- package/cli/lib/process-manager.js +176 -0
- package/cli/lib/utils.js +78 -0
- package/cli/lib/validators.js +219 -0
- package/cli/lib/voice-downloader.js +79 -0
- package/docker/entrypoint.sh +110 -0
- package/docker/supervisord.conf +78 -0
- package/docker-compose.yml +40 -0
- package/docs/CLAUDE-CODE-SKILL.md +45 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/MCP-SERVER.md +111 -0
- package/docs/TROUBLESHOOTING.md +288 -0
- package/mcp-server/index.js +159 -0
- package/mcp-server/package-lock.json +1201 -0
- package/mcp-server/package.json +9 -0
- package/package.json +73 -0
- package/stt-local/Dockerfile +12 -0
- package/stt-local/server.py +45 -0
- package/tts-local/Dockerfile +9 -0
- package/tts-local/server.py +83 -0
- package/voice-app/API-QUERY-CONTRACT.md +238 -0
- package/voice-app/DEPLOYMENT.md +176 -0
- package/voice-app/Dockerfile +20 -0
- package/voice-app/README-OUTBOUND.md +316 -0
- package/voice-app/config/devices.json.example +18 -0
- package/voice-app/index.js +273 -0
- package/voice-app/lib/audio-fork.js +465 -0
- package/voice-app/lib/claude-bridge.js +113 -0
- package/voice-app/lib/connection-retry.js +58 -0
- package/voice-app/lib/conversation-loop.js +490 -0
- package/voice-app/lib/device-registry.js +171 -0
- package/voice-app/lib/http-server.js +242 -0
- package/voice-app/lib/logger.js +35 -0
- package/voice-app/lib/multi-registrar.js +133 -0
- package/voice-app/lib/outbound-handler.js +299 -0
- package/voice-app/lib/outbound-routes.js +417 -0
- package/voice-app/lib/outbound-session.js +324 -0
- package/voice-app/lib/query-routes.js +484 -0
- package/voice-app/lib/registrar.js +144 -0
- package/voice-app/lib/sip-handler.js +490 -0
- package/voice-app/lib/tts-service.js +205 -0
- package/voice-app/lib/whisper-client.js +140 -0
- package/voice-app/package.json +38 -0
- package/voice-app/static/gotit-beep.wav +0 -0
- package/voice-app/static/hold-music.wav +0 -0
- package/voice-app/static/ready-beep.wav +0 -0
- package/voice-app/test/freeswitch-retry.test.js +100 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { loadConfig, configExists, getInstallationType } from '../config.js';
|
|
6
|
+
import { checkDocker, writeDockerConfig, startContainers } from '../docker.js';
|
|
7
|
+
import { startServer, isServerRunning } from '../process-manager.js';
|
|
8
|
+
import { isClaudeInstalled, sleep } from '../utils.js';
|
|
9
|
+
import { checkClaudeApiServer } from '../network.js';
|
|
10
|
+
import { runPrereqChecks } from '../prereqs.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Start command - Launch all services
|
|
14
|
+
* @returns {Promise<void>}
|
|
15
|
+
*/
|
|
16
|
+
export async function startCommand() {
|
|
17
|
+
console.log(chalk.bold.cyan('\n🚀 Starting Claude Phone\n'));
|
|
18
|
+
|
|
19
|
+
// Check if configured
|
|
20
|
+
if (!configExists()) {
|
|
21
|
+
console.log(chalk.red('✗ Configuration not found'));
|
|
22
|
+
console.log(chalk.gray(' Run "claude-phone setup" first\n'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Load config and get installation type
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
const installationType = getInstallationType(config);
|
|
29
|
+
const isPiMode = config.deployment?.mode === 'pi-split';
|
|
30
|
+
|
|
31
|
+
console.log(chalk.gray(`Installation type: ${installationType}\n`));
|
|
32
|
+
|
|
33
|
+
// Run prerequisite checks for this installation type
|
|
34
|
+
const prereqResult = await runPrereqChecks({ type: installationType });
|
|
35
|
+
if (!prereqResult.success) {
|
|
36
|
+
console.log(chalk.red('\n❌ Prerequisites not met. Please run "claude-phone setup" to fix.\n'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isPiMode) {
|
|
41
|
+
console.log(chalk.cyan('🥧 Pi Split-Mode detected\n'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Route to type-specific start function
|
|
45
|
+
switch (installationType) {
|
|
46
|
+
case 'api-server':
|
|
47
|
+
await startApiServer(config);
|
|
48
|
+
break;
|
|
49
|
+
case 'voice-server':
|
|
50
|
+
await startVoiceServer(config, isPiMode);
|
|
51
|
+
break;
|
|
52
|
+
case 'both':
|
|
53
|
+
default:
|
|
54
|
+
await startBoth(config, isPiMode);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Start API server only
|
|
61
|
+
* @param {object} config - Configuration
|
|
62
|
+
* @returns {Promise<void>}
|
|
63
|
+
*/
|
|
64
|
+
async function startApiServer(config) {
|
|
65
|
+
// Check Claude CLI
|
|
66
|
+
if (!(await isClaudeInstalled())) {
|
|
67
|
+
console.log(chalk.yellow('⚠️ Claude CLI not found'));
|
|
68
|
+
console.log(chalk.gray(' Install from: https://claude.com/download\n'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify path exists
|
|
72
|
+
if (!fs.existsSync(config.paths.claudeApiServer)) {
|
|
73
|
+
console.log(chalk.red(`✗ Claude API server not found at: ${config.paths.claudeApiServer}`));
|
|
74
|
+
console.log(chalk.gray(' Update paths in configuration\n'));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check if dependencies are installed
|
|
79
|
+
const nodeModulesPath = path.join(config.paths.claudeApiServer, 'node_modules');
|
|
80
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
81
|
+
console.log(chalk.red('✗ Dependencies not installed in claude-api-server'));
|
|
82
|
+
console.log(chalk.yellow('\nRun the following to install dependencies:'));
|
|
83
|
+
console.log(chalk.cyan(` cd ${config.paths.claudeApiServer} && npm install\n`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Start claude-api-server
|
|
88
|
+
const spinner = ora('Starting Claude API server...').start();
|
|
89
|
+
try {
|
|
90
|
+
if (await isServerRunning()) {
|
|
91
|
+
spinner.warn('Claude API server already running');
|
|
92
|
+
} else {
|
|
93
|
+
await startServer(config.paths.claudeApiServer, config.server.claudeApiPort);
|
|
94
|
+
spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
spinner.fail(`Failed to start server: ${error.message}`);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Success
|
|
102
|
+
console.log(chalk.bold.green('\n✓ API server running!\n'));
|
|
103
|
+
console.log(chalk.gray('Service:'));
|
|
104
|
+
console.log(chalk.gray(` • Claude API server: http://localhost:${config.server.claudeApiPort}\n`));
|
|
105
|
+
console.log(chalk.gray('Voice servers can connect to this API server.\n'));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Start voice server only
|
|
110
|
+
* @param {object} config - Configuration
|
|
111
|
+
* @param {boolean} isPiMode - Is Pi split-mode
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
*/
|
|
114
|
+
async function startVoiceServer(config, isPiMode) {
|
|
115
|
+
// Verify voice-app path exists
|
|
116
|
+
if (!fs.existsSync(config.paths.voiceApp)) {
|
|
117
|
+
console.log(chalk.red(`✗ Voice app not found at: ${config.paths.voiceApp}`));
|
|
118
|
+
console.log(chalk.gray(' Update paths in configuration\n'));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// In Pi mode or voice-server mode, check API server reachability
|
|
123
|
+
const apiServerIp = isPiMode ? config.deployment.pi.macIp : config.deployment.apiServerIp;
|
|
124
|
+
if (apiServerIp) {
|
|
125
|
+
const apiServerUrl = `http://${apiServerIp}:${config.server.claudeApiPort}`;
|
|
126
|
+
const apiSpinner = ora(`Checking API server at ${apiServerUrl}...`).start();
|
|
127
|
+
const apiHealth = await checkClaudeApiServer(apiServerUrl);
|
|
128
|
+
if (apiHealth.healthy) {
|
|
129
|
+
apiSpinner.succeed(`API server is healthy at ${apiServerUrl}`);
|
|
130
|
+
} else {
|
|
131
|
+
apiSpinner.warn(`API server not responding at ${apiServerUrl}`);
|
|
132
|
+
console.log(chalk.yellow(' ⚠️ Make sure "claude-phone api-server" is running on your API server\n'));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check Docker
|
|
137
|
+
const spinner = ora('Checking Docker...').start();
|
|
138
|
+
const dockerStatus = await checkDocker();
|
|
139
|
+
|
|
140
|
+
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
141
|
+
spinner.fail(dockerStatus.error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
spinner.succeed('Docker is ready');
|
|
145
|
+
|
|
146
|
+
// Generate Docker config
|
|
147
|
+
spinner.start('Generating Docker configuration...');
|
|
148
|
+
try {
|
|
149
|
+
await writeDockerConfig(config);
|
|
150
|
+
|
|
151
|
+
// Also write devices.json to voice-app/config
|
|
152
|
+
const devicesPath = path.join(config.paths.voiceApp, 'config', 'devices.json');
|
|
153
|
+
const devicesConfig = {};
|
|
154
|
+
for (const device of config.devices) {
|
|
155
|
+
devicesConfig[device.extension] = device;
|
|
156
|
+
}
|
|
157
|
+
await fs.promises.writeFile(devicesPath, JSON.stringify(devicesConfig, null, 2), { mode: 0o644 });
|
|
158
|
+
|
|
159
|
+
spinner.succeed('Docker configuration generated');
|
|
160
|
+
} catch (error) {
|
|
161
|
+
spinner.fail(`Failed to generate config: ${error.message}`);
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Start Docker containers
|
|
166
|
+
spinner.start('Starting Docker containers...');
|
|
167
|
+
try {
|
|
168
|
+
await startContainers();
|
|
169
|
+
spinner.succeed('Docker containers started');
|
|
170
|
+
} catch (error) {
|
|
171
|
+
spinner.fail(`Failed to start containers: ${error.message}`);
|
|
172
|
+
|
|
173
|
+
if (error.message.includes('port') || error.message.includes('address already in use')) {
|
|
174
|
+
console.log(chalk.yellow('\n⚠️ Port conflict detected\n'));
|
|
175
|
+
console.log(chalk.gray('Possible causes:'));
|
|
176
|
+
console.log(chalk.gray(' • 3CX SBC is running on the configured port'));
|
|
177
|
+
console.log(chalk.gray(' • Another service is using the port'));
|
|
178
|
+
console.log(chalk.gray('\nSuggested fixes:'));
|
|
179
|
+
console.log(chalk.gray(' 1. If 3CX SBC is on port 5060, run "claude-phone setup" again'));
|
|
180
|
+
console.log(chalk.gray(' 2. Check running containers: docker ps'));
|
|
181
|
+
console.log(chalk.gray(' 3. Stop conflicting services: docker compose down\n'));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Wait a bit for containers to initialize
|
|
188
|
+
spinner.start('Waiting for containers to initialize...');
|
|
189
|
+
await sleep(3000);
|
|
190
|
+
spinner.succeed('Containers initialized');
|
|
191
|
+
|
|
192
|
+
// Success
|
|
193
|
+
console.log(chalk.bold.green('\n✓ Voice server running!\n'));
|
|
194
|
+
console.log(chalk.gray('Services:'));
|
|
195
|
+
console.log(chalk.gray(` • Docker containers: drachtio, freeswitch, voice-app`));
|
|
196
|
+
if (apiServerIp) {
|
|
197
|
+
console.log(chalk.gray(` • API server: http://${apiServerIp}:${config.server.claudeApiPort}`));
|
|
198
|
+
}
|
|
199
|
+
console.log(chalk.gray(` • Voice app API: http://localhost:${config.server.httpPort}\n`));
|
|
200
|
+
console.log(chalk.gray('Ready to receive calls on:'));
|
|
201
|
+
for (const device of config.devices) {
|
|
202
|
+
console.log(chalk.gray(` • ${device.name}: extension ${device.extension}`));
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start both API server and voice server
|
|
209
|
+
* @param {object} config - Configuration
|
|
210
|
+
* @param {boolean} isPiMode - Is Pi split-mode
|
|
211
|
+
* @returns {Promise<void>}
|
|
212
|
+
*/
|
|
213
|
+
async function startBoth(config, isPiMode) {
|
|
214
|
+
// Verify voice-app path exists
|
|
215
|
+
if (!fs.existsSync(config.paths.voiceApp)) {
|
|
216
|
+
console.log(chalk.red(`✗ Voice app not found at: ${config.paths.voiceApp}`));
|
|
217
|
+
console.log(chalk.gray(' Update paths in configuration\n'));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Only check claude-api-server path in standard mode (not Pi mode)
|
|
222
|
+
if (!isPiMode && !fs.existsSync(config.paths.claudeApiServer)) {
|
|
223
|
+
console.log(chalk.red(`✗ Claude API server not found at: ${config.paths.claudeApiServer}`));
|
|
224
|
+
console.log(chalk.gray(' Update paths in configuration\n'));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check if dependencies are installed (not in Pi mode)
|
|
229
|
+
if (!isPiMode) {
|
|
230
|
+
const nodeModulesPath = path.join(config.paths.claudeApiServer, 'node_modules');
|
|
231
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
232
|
+
console.log(chalk.red('✗ Dependencies not installed in claude-api-server'));
|
|
233
|
+
console.log(chalk.yellow('\nRun the following to install dependencies:'));
|
|
234
|
+
console.log(chalk.cyan(` cd ${config.paths.claudeApiServer} && npm install\n`));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check Claude CLI only in standard mode (Pi mode connects to API server instead)
|
|
240
|
+
if (!isPiMode && !(await isClaudeInstalled())) {
|
|
241
|
+
console.log(chalk.yellow('⚠️ Claude CLI not found'));
|
|
242
|
+
console.log(chalk.gray(' Install from: https://claude.com/download\n'));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// In Pi mode, verify API server is reachable
|
|
246
|
+
if (isPiMode) {
|
|
247
|
+
const apiServerUrl = `http://${config.deployment.pi.macIp}:${config.server.claudeApiPort}`;
|
|
248
|
+
const apiSpinner = ora(`Checking API server at ${apiServerUrl}...`).start();
|
|
249
|
+
const apiHealth = await checkClaudeApiServer(apiServerUrl);
|
|
250
|
+
if (apiHealth.healthy) {
|
|
251
|
+
apiSpinner.succeed(`API server is healthy at ${apiServerUrl}`);
|
|
252
|
+
} else {
|
|
253
|
+
apiSpinner.warn(`API server not responding at ${apiServerUrl}`);
|
|
254
|
+
console.log(chalk.yellow(' ⚠️ Make sure "claude-phone api-server" is running on your API server\n'));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check Docker
|
|
259
|
+
const spinner = ora('Checking Docker...').start();
|
|
260
|
+
const dockerStatus = await checkDocker();
|
|
261
|
+
|
|
262
|
+
if (!dockerStatus.installed || !dockerStatus.running) {
|
|
263
|
+
spinner.fail(dockerStatus.error);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
spinner.succeed('Docker is ready');
|
|
267
|
+
|
|
268
|
+
// Generate Docker config
|
|
269
|
+
spinner.start('Generating Docker configuration...');
|
|
270
|
+
try {
|
|
271
|
+
await writeDockerConfig(config);
|
|
272
|
+
|
|
273
|
+
// Also write devices.json to voice-app/config
|
|
274
|
+
const devicesPath = path.join(config.paths.voiceApp, 'config', 'devices.json');
|
|
275
|
+
const devicesConfig = {};
|
|
276
|
+
for (const device of config.devices) {
|
|
277
|
+
devicesConfig[device.extension] = device;
|
|
278
|
+
}
|
|
279
|
+
await fs.promises.writeFile(devicesPath, JSON.stringify(devicesConfig, null, 2), { mode: 0o644 });
|
|
280
|
+
|
|
281
|
+
spinner.succeed('Docker configuration generated');
|
|
282
|
+
} catch (error) {
|
|
283
|
+
spinner.fail(`Failed to generate config: ${error.message}`);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Start Docker containers
|
|
288
|
+
spinner.start('Starting Docker containers...');
|
|
289
|
+
try {
|
|
290
|
+
await startContainers();
|
|
291
|
+
spinner.succeed('Docker containers started');
|
|
292
|
+
} catch (error) {
|
|
293
|
+
spinner.fail(`Failed to start containers: ${error.message}`);
|
|
294
|
+
|
|
295
|
+
// AC25: Detect drachtio port conflict
|
|
296
|
+
if (error.message.includes('port') || error.message.includes('address already in use')) {
|
|
297
|
+
console.log(chalk.yellow('\n⚠️ Port conflict detected\n'));
|
|
298
|
+
console.log(chalk.gray('Possible causes:'));
|
|
299
|
+
console.log(chalk.gray(' • 3CX SBC is running on the configured port'));
|
|
300
|
+
console.log(chalk.gray(' • Another service is using the port'));
|
|
301
|
+
console.log(chalk.gray('\nSuggested fixes:'));
|
|
302
|
+
console.log(chalk.gray(' 1. If 3CX SBC is on port 5060, run "claude-phone setup" again'));
|
|
303
|
+
console.log(chalk.gray(' 2. Check running containers: docker ps'));
|
|
304
|
+
console.log(chalk.gray(' 3. Stop conflicting services: docker compose down\n'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Wait a bit for containers to initialize
|
|
311
|
+
spinner.start('Waiting for containers to initialize...');
|
|
312
|
+
await sleep(3000);
|
|
313
|
+
spinner.succeed('Containers initialized');
|
|
314
|
+
|
|
315
|
+
// Start claude-api-server (only in standard mode - Pi mode uses remote API server)
|
|
316
|
+
if (!isPiMode) {
|
|
317
|
+
spinner.start('Starting Claude API server...');
|
|
318
|
+
try {
|
|
319
|
+
if (await isServerRunning()) {
|
|
320
|
+
spinner.warn('Claude API server already running');
|
|
321
|
+
} else {
|
|
322
|
+
await startServer(config.paths.claudeApiServer, config.server.claudeApiPort);
|
|
323
|
+
spinner.succeed(`Claude API server started on port ${config.server.claudeApiPort}`);
|
|
324
|
+
}
|
|
325
|
+
} catch (error) {
|
|
326
|
+
spinner.fail(`Failed to start server: ${error.message}`);
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Success
|
|
332
|
+
console.log(chalk.bold.green('\n✓ All services running!\n'));
|
|
333
|
+
console.log(chalk.gray('Services:'));
|
|
334
|
+
console.log(chalk.gray(` • Docker containers: drachtio, freeswitch, voice-app`));
|
|
335
|
+
if (isPiMode) {
|
|
336
|
+
console.log(chalk.gray(` • API server: http://${config.deployment.pi.macIp}:${config.server.claudeApiPort}`));
|
|
337
|
+
} else {
|
|
338
|
+
console.log(chalk.gray(` • Claude API server: http://localhost:${config.server.claudeApiPort}`));
|
|
339
|
+
}
|
|
340
|
+
console.log(chalk.gray(` • Voice app API: http://localhost:${config.server.httpPort}\n`));
|
|
341
|
+
console.log(chalk.gray('Ready to receive calls on:'));
|
|
342
|
+
for (const device of config.devices) {
|
|
343
|
+
console.log(chalk.gray(` • ${device.name}: extension ${device.extension}`));
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig, configExists, getInstallationType } from '../config.js';
|
|
3
|
+
import { getContainerStatus } from '../docker.js';
|
|
4
|
+
import { isServerRunning, getServerPid } from '../process-manager.js';
|
|
5
|
+
import { checkClaudeApiServer } from '../network.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Status command - Show service status
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export async function statusCommand() {
|
|
12
|
+
console.log(chalk.bold.cyan('\n📊 Claude Phone Status\n'));
|
|
13
|
+
|
|
14
|
+
// Check if configured
|
|
15
|
+
if (!configExists()) {
|
|
16
|
+
console.log(chalk.red('✗ Not configured'));
|
|
17
|
+
console.log(chalk.gray(' Run "claude-phone setup" first\n'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
const installationType = getInstallationType(config);
|
|
23
|
+
const isPiSplit = config.deployment && config.deployment.mode === 'pi-split';
|
|
24
|
+
|
|
25
|
+
// Show installation type
|
|
26
|
+
console.log(chalk.bold('Installation Type:'));
|
|
27
|
+
console.log(chalk.gray(` ${installationType === 'api-server' ? 'API Server' : installationType === 'voice-server' ? 'Voice Server' : 'Both (all-in-one)'}`));
|
|
28
|
+
console.log();
|
|
29
|
+
|
|
30
|
+
// Show type-appropriate status
|
|
31
|
+
if (installationType === 'api-server' || installationType === 'both') {
|
|
32
|
+
await showApiServerStatus(config, isPiSplit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (installationType === 'voice-server' || installationType === 'both') {
|
|
36
|
+
await showVoiceServerStatus(config, isPiSplit, installationType);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Show API server status
|
|
44
|
+
* @param {object} config - Configuration
|
|
45
|
+
* @param {boolean} isPiSplit - Is Pi split mode
|
|
46
|
+
* @returns {Promise<void>}
|
|
47
|
+
*/
|
|
48
|
+
async function showApiServerStatus(config, isPiSplit) {
|
|
49
|
+
console.log(chalk.bold('Claude API Server:'));
|
|
50
|
+
|
|
51
|
+
if (isPiSplit) {
|
|
52
|
+
// Pi-split mode: Check remote API server
|
|
53
|
+
const apiUrl = `http://${config.deployment.pi.macIp}:${config.server.claudeApiPort}`;
|
|
54
|
+
const apiHealth = await checkClaudeApiServer(apiUrl);
|
|
55
|
+
|
|
56
|
+
if (apiHealth.healthy) {
|
|
57
|
+
console.log(chalk.green(` ✓ Connected to API server (${config.deployment.pi.macIp}:${config.server.claudeApiPort})`));
|
|
58
|
+
console.log(chalk.gray(' Remote API server is healthy'));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.red(` ✗ Cannot reach API server`));
|
|
61
|
+
console.log(chalk.gray(` Tried: ${apiUrl}`));
|
|
62
|
+
console.log(chalk.gray(' Run "claude-phone api-server" on your API server'));
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// Standard mode: Check local server
|
|
66
|
+
const serverRunning = await isServerRunning();
|
|
67
|
+
if (serverRunning) {
|
|
68
|
+
const pid = getServerPid();
|
|
69
|
+
console.log(chalk.green(` ✓ Running (PID: ${pid})`));
|
|
70
|
+
console.log(chalk.gray(` Port: ${config.server.claudeApiPort}`));
|
|
71
|
+
} else {
|
|
72
|
+
console.log(chalk.red(' ✗ Not running'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Show voice server status
|
|
80
|
+
* @param {object} config - Configuration
|
|
81
|
+
* @param {boolean} isPiSplit - Is Pi split mode
|
|
82
|
+
* @param {string} installationType - Installation type
|
|
83
|
+
* @returns {Promise<void>}
|
|
84
|
+
*/
|
|
85
|
+
async function showVoiceServerStatus(config, isPiSplit, installationType) {
|
|
86
|
+
// Docker Containers
|
|
87
|
+
console.log(chalk.bold('Docker Containers:'));
|
|
88
|
+
const containers = await getContainerStatus();
|
|
89
|
+
|
|
90
|
+
if (containers.length === 0) {
|
|
91
|
+
console.log(chalk.red(' ✗ No containers running'));
|
|
92
|
+
} else {
|
|
93
|
+
for (const container of containers) {
|
|
94
|
+
const isRunning = container.status.toLowerCase().includes('up') ||
|
|
95
|
+
container.status.toLowerCase().includes('running');
|
|
96
|
+
const icon = isRunning ? '✓' : '✗';
|
|
97
|
+
const color = isRunning ? chalk.green : chalk.red;
|
|
98
|
+
console.log(color(` ${icon} ${container.name}: ${container.status}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
|
|
103
|
+
// Devices
|
|
104
|
+
console.log(chalk.bold('Configured Devices:'));
|
|
105
|
+
if (config.devices && config.devices.length > 0) {
|
|
106
|
+
for (const device of config.devices) {
|
|
107
|
+
console.log(chalk.gray(` • ${device.name} (extension ${device.extension})`));
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
console.log(chalk.gray(' (none configured)'));
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
|
|
114
|
+
// Network
|
|
115
|
+
console.log(chalk.bold('Network:'));
|
|
116
|
+
if (isPiSplit) {
|
|
117
|
+
console.log(chalk.gray(` Deployment Mode: Pi Split`));
|
|
118
|
+
console.log(chalk.gray(` Pi IP: ${config.server.externalIp}`));
|
|
119
|
+
console.log(chalk.gray(` API Server IP: ${config.deployment.pi.macIp}`));
|
|
120
|
+
console.log(chalk.gray(` Drachtio Port: ${config.deployment.pi.drachtioPort}`));
|
|
121
|
+
if (config.deployment.pi.has3cxSbc) {
|
|
122
|
+
console.log(chalk.yellow(' 3CX SBC detected (using port 5070)'));
|
|
123
|
+
}
|
|
124
|
+
} else if (installationType === 'voice-server') {
|
|
125
|
+
console.log(chalk.gray(` Deployment Mode: Voice Server`));
|
|
126
|
+
console.log(chalk.gray(` Server IP: ${config.server.externalIp}`));
|
|
127
|
+
if (config.deployment.apiServerIp) {
|
|
128
|
+
console.log(chalk.gray(` API Server IP: ${config.deployment.apiServerIp}`));
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.gray(` Deployment Mode: Both (all-in-one)`));
|
|
132
|
+
console.log(chalk.gray(` External IP: ${config.server.externalIp}`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (config.sip) {
|
|
136
|
+
console.log(chalk.gray(` SIP Domain: ${config.sip.domain}`));
|
|
137
|
+
console.log(chalk.gray(` SIP Registrar: ${config.sip.registrar}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { loadConfig, configExists, getInstallationType } from '../config.js';
|
|
4
|
+
import { stopContainers } from '../docker.js';
|
|
5
|
+
import { stopServer, isServerRunning } from '../process-manager.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stop command - Shut down all services
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export async function stopCommand() {
|
|
12
|
+
console.log(chalk.bold.cyan('\n⏹️ Stopping Claude Phone\n'));
|
|
13
|
+
|
|
14
|
+
// Check if configured
|
|
15
|
+
if (!configExists()) {
|
|
16
|
+
console.log(chalk.yellow('⚠️ Not configured. Nothing to stop.\n'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Load config and get installation type
|
|
21
|
+
const config = await loadConfig();
|
|
22
|
+
const installationType = getInstallationType(config);
|
|
23
|
+
|
|
24
|
+
console.log(chalk.gray(`Installation type: ${installationType}\n`));
|
|
25
|
+
|
|
26
|
+
// Route to type-specific stop function
|
|
27
|
+
switch (installationType) {
|
|
28
|
+
case 'api-server':
|
|
29
|
+
await stopApiServer();
|
|
30
|
+
break;
|
|
31
|
+
case 'voice-server':
|
|
32
|
+
await stopVoiceServer();
|
|
33
|
+
break;
|
|
34
|
+
case 'both':
|
|
35
|
+
default:
|
|
36
|
+
await stopBoth();
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.bold.green('\n✓ Services stopped\n'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Stop API server only
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async function stopApiServer() {
|
|
48
|
+
const spinner = ora('Stopping Claude API server...').start();
|
|
49
|
+
try {
|
|
50
|
+
if (await isServerRunning()) {
|
|
51
|
+
await stopServer();
|
|
52
|
+
spinner.succeed('Claude API server stopped');
|
|
53
|
+
} else {
|
|
54
|
+
spinner.info('Claude API server not running');
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
spinner.fail(`Failed to stop server: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stop voice server only
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async function stopVoiceServer() {
|
|
66
|
+
const spinner = ora('Stopping Docker containers...').start();
|
|
67
|
+
try {
|
|
68
|
+
await stopContainers();
|
|
69
|
+
spinner.succeed('Docker containers stopped');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
spinner.fail(`Failed to stop containers: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop both API server and voice server
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async function stopBoth() {
|
|
80
|
+
// Stop claude-api-server
|
|
81
|
+
const spinner = ora('Stopping Claude API server...').start();
|
|
82
|
+
try {
|
|
83
|
+
if (await isServerRunning()) {
|
|
84
|
+
await stopServer();
|
|
85
|
+
spinner.succeed('Claude API server stopped');
|
|
86
|
+
} else {
|
|
87
|
+
spinner.info('Claude API server not running');
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
spinner.fail(`Failed to stop server: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Stop Docker containers
|
|
94
|
+
spinner.start('Stopping Docker containers...');
|
|
95
|
+
try {
|
|
96
|
+
await stopContainers();
|
|
97
|
+
spinner.succeed('Docker containers stopped');
|
|
98
|
+
} catch (error) {
|
|
99
|
+
spinner.fail(`Failed to stop containers: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|