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.
Files changed (96) hide show
  1. package/.env.example +108 -0
  2. package/Dockerfile +84 -0
  3. package/LICENSE +22 -0
  4. package/README.md +231 -0
  5. package/claude-api-server/package-lock.json +832 -0
  6. package/claude-api-server/package.json +20 -0
  7. package/claude-api-server/server.js +540 -0
  8. package/claude-api-server/structured.js +208 -0
  9. package/cli/README.md +231 -0
  10. package/cli/bin/check-publish-safety.js +53 -0
  11. package/cli/bin/claude-phone.js +23 -0
  12. package/cli/bin/cli-main.js +284 -0
  13. package/cli/bin/postinstall.js +20 -0
  14. package/cli/lib/commands/api-server.js +77 -0
  15. package/cli/lib/commands/backup.js +69 -0
  16. package/cli/lib/commands/config/path.js +30 -0
  17. package/cli/lib/commands/config/reset.js +73 -0
  18. package/cli/lib/commands/config/show.js +86 -0
  19. package/cli/lib/commands/device/add.js +143 -0
  20. package/cli/lib/commands/device/list.js +55 -0
  21. package/cli/lib/commands/device/remove.js +83 -0
  22. package/cli/lib/commands/doctor.js +422 -0
  23. package/cli/lib/commands/logs.js +186 -0
  24. package/cli/lib/commands/restore.js +172 -0
  25. package/cli/lib/commands/setup.js +1246 -0
  26. package/cli/lib/commands/start.js +346 -0
  27. package/cli/lib/commands/status.js +139 -0
  28. package/cli/lib/commands/stop.js +101 -0
  29. package/cli/lib/commands/uninstall.js +183 -0
  30. package/cli/lib/commands/update.js +205 -0
  31. package/cli/lib/config.js +113 -0
  32. package/cli/lib/docker.js +384 -0
  33. package/cli/lib/mcp-register.js +108 -0
  34. package/cli/lib/network.js +109 -0
  35. package/cli/lib/platform.js +85 -0
  36. package/cli/lib/port-check.js +135 -0
  37. package/cli/lib/prereqs/checks/compose.js +147 -0
  38. package/cli/lib/prereqs/checks/disk.js +89 -0
  39. package/cli/lib/prereqs/checks/docker.js +155 -0
  40. package/cli/lib/prereqs/checks/network.js +78 -0
  41. package/cli/lib/prereqs/checks/node.js +95 -0
  42. package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
  43. package/cli/lib/prereqs/installers/docker.js +229 -0
  44. package/cli/lib/prereqs/installers/node.js +254 -0
  45. package/cli/lib/prereqs/platform.js +175 -0
  46. package/cli/lib/prereqs/utils/execute.js +208 -0
  47. package/cli/lib/prereqs/utils/rollback.js +223 -0
  48. package/cli/lib/prereqs/utils/sudo.js +177 -0
  49. package/cli/lib/prereqs.js +228 -0
  50. package/cli/lib/prerequisites.js +115 -0
  51. package/cli/lib/process-manager.js +176 -0
  52. package/cli/lib/utils.js +78 -0
  53. package/cli/lib/validators.js +219 -0
  54. package/cli/lib/voice-downloader.js +79 -0
  55. package/docker/entrypoint.sh +110 -0
  56. package/docker/supervisord.conf +78 -0
  57. package/docker-compose.yml +40 -0
  58. package/docs/CLAUDE-CODE-SKILL.md +45 -0
  59. package/docs/LANGUAGES.md +113 -0
  60. package/docs/MCP-SERVER.md +111 -0
  61. package/docs/TROUBLESHOOTING.md +288 -0
  62. package/mcp-server/index.js +159 -0
  63. package/mcp-server/package-lock.json +1201 -0
  64. package/mcp-server/package.json +9 -0
  65. package/package.json +73 -0
  66. package/stt-local/Dockerfile +12 -0
  67. package/stt-local/server.py +45 -0
  68. package/tts-local/Dockerfile +9 -0
  69. package/tts-local/server.py +83 -0
  70. package/voice-app/API-QUERY-CONTRACT.md +238 -0
  71. package/voice-app/DEPLOYMENT.md +176 -0
  72. package/voice-app/Dockerfile +20 -0
  73. package/voice-app/README-OUTBOUND.md +316 -0
  74. package/voice-app/config/devices.json.example +18 -0
  75. package/voice-app/index.js +273 -0
  76. package/voice-app/lib/audio-fork.js +465 -0
  77. package/voice-app/lib/claude-bridge.js +113 -0
  78. package/voice-app/lib/connection-retry.js +58 -0
  79. package/voice-app/lib/conversation-loop.js +490 -0
  80. package/voice-app/lib/device-registry.js +171 -0
  81. package/voice-app/lib/http-server.js +242 -0
  82. package/voice-app/lib/logger.js +35 -0
  83. package/voice-app/lib/multi-registrar.js +133 -0
  84. package/voice-app/lib/outbound-handler.js +299 -0
  85. package/voice-app/lib/outbound-routes.js +417 -0
  86. package/voice-app/lib/outbound-session.js +324 -0
  87. package/voice-app/lib/query-routes.js +484 -0
  88. package/voice-app/lib/registrar.js +144 -0
  89. package/voice-app/lib/sip-handler.js +490 -0
  90. package/voice-app/lib/tts-service.js +205 -0
  91. package/voice-app/lib/whisper-client.js +140 -0
  92. package/voice-app/package.json +38 -0
  93. package/voice-app/static/gotit-beep.wav +0 -0
  94. package/voice-app/static/hold-music.wav +0 -0
  95. package/voice-app/static/ready-beep.wav +0 -0
  96. 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
+ }