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,284 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { setupCommand } from '../lib/commands/setup.js';
4
+ import { startCommand } from '../lib/commands/start.js';
5
+ import { stopCommand } from '../lib/commands/stop.js';
6
+ import { statusCommand } from '../lib/commands/status.js';
7
+ import { doctorCommand } from '../lib/commands/doctor.js';
8
+ import { apiServerCommand } from '../lib/commands/api-server.js';
9
+ import { deviceAddCommand } from '../lib/commands/device/add.js';
10
+ import { deviceListCommand } from '../lib/commands/device/list.js';
11
+ import { deviceRemoveCommand } from '../lib/commands/device/remove.js';
12
+ import { logsCommand } from '../lib/commands/logs.js';
13
+ import { configShowCommand } from '../lib/commands/config/show.js';
14
+ import { configPathCommand } from '../lib/commands/config/path.js';
15
+ import { configResetCommand } from '../lib/commands/config/reset.js';
16
+ import { updateCommand } from '../lib/commands/update.js';
17
+ import { backupCommand } from '../lib/commands/backup.js';
18
+ import { restoreCommand } from '../lib/commands/restore.js';
19
+ import { uninstallCommand } from '../lib/commands/uninstall.js';
20
+ import { registerMcpServerWithOutput, mcpServerPath } from '../lib/mcp-register.js';
21
+ import { loadConfig as _loadCfgForMcp } from '../lib/config.js';
22
+
23
+ const program = new Command();
24
+
25
+ program
26
+ .name('claude-phone')
27
+ .description('Voice interface for Claude Code via SIP - Call your AI, and your AI can call you')
28
+ .version('1.0.0');
29
+
30
+ program
31
+ .command('setup')
32
+ .description('Interactive setup wizard for API keys, 3CX config, and devices')
33
+ .option('--skip-prereqs', 'Skip prerequisite checks (advanced users only)')
34
+ .action(async (options) => {
35
+ try {
36
+ await setupCommand(options);
37
+ } catch (error) {
38
+ console.error(chalk.red(`\n✗ Setup failed: ${error.message}\n`));
39
+ process.exit(1);
40
+ }
41
+ });
42
+
43
+ program
44
+ .command('start')
45
+ .description('Start all services (Docker containers + claude-api-server)')
46
+ .action(async () => {
47
+ try {
48
+ await startCommand();
49
+ } catch (error) {
50
+ console.error(chalk.red(`\n✗ Start failed: ${error.message}\n`));
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ program
56
+ .command('stop')
57
+ .description('Stop all services')
58
+ .action(async () => {
59
+ try {
60
+ await stopCommand();
61
+ } catch (error) {
62
+ console.error(chalk.red(`\n✗ Stop failed: ${error.message}\n`));
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ program
68
+ .command('status')
69
+ .description('Show status of all services')
70
+ .action(async () => {
71
+ try {
72
+ await statusCommand();
73
+ } catch (error) {
74
+ console.error(chalk.red(`\n✗ Status check failed: ${error.message}\n`));
75
+ process.exit(1);
76
+ }
77
+ });
78
+
79
+ program
80
+ .command('doctor')
81
+ .description('Run health checks on all dependencies and services')
82
+ .action(async () => {
83
+ try {
84
+ await doctorCommand();
85
+ } catch (error) {
86
+ console.error(chalk.red(`\n✗ Health check failed: ${error.message}\n`));
87
+ process.exit(1);
88
+ }
89
+ });
90
+
91
+ program
92
+ .command('api-server')
93
+ .description('Start Claude API server for Pi remote connections')
94
+ .option('-p, --port <port>', 'Port to listen on', '3333')
95
+ .action(async (options) => {
96
+ try {
97
+ const port = parseInt(options.port, 10);
98
+ if (isNaN(port) || port < 1024 || port > 65535) {
99
+ console.error(chalk.red('\n✗ Port must be between 1024 and 65535\n'));
100
+ process.exit(1);
101
+ }
102
+ await apiServerCommand({ port });
103
+ } catch (error) {
104
+ console.error(chalk.red(`\n✗ API server failed: ${error.message}\n`));
105
+ process.exit(1);
106
+ }
107
+ });
108
+
109
+ // Device management subcommands
110
+ const mcp = program
111
+ .command('mcp')
112
+ .description('Manage the MCP server that lets Claude Code call you');
113
+
114
+ mcp
115
+ .command('install')
116
+ .description('Register the MCP server with Claude Code')
117
+ .option('-t, --to <number>', 'Extension or number Claude should ring')
118
+ .option('-f, --force', 'Re-register even if already present')
119
+ .action(async (options) => {
120
+ let cfg = {};
121
+ try { cfg = await _loadCfgForMcp(); } catch { /* not set up yet - fine */ }
122
+ const firstDevice = cfg.devices && Object.keys(cfg.devices).length
123
+ ? Object.values(cfg.devices)[0]
124
+ : null;
125
+ registerMcpServerWithOutput({
126
+ defaultTo: options.to || cfg.sip?.callbackNumber || firstDevice?.extension || cfg.sip?.extension,
127
+ httpPort: cfg.server?.httpPort || 3000,
128
+ force: !!options.force,
129
+ });
130
+ });
131
+
132
+ mcp
133
+ .command('path')
134
+ .description('Print the MCP server path (for manual registration)')
135
+ .action(() => console.log(mcpServerPath()));
136
+
137
+ const device = program
138
+ .command('device')
139
+ .description('Manage SIP devices');
140
+
141
+ device
142
+ .command('add')
143
+ .description('Add a new device')
144
+ .action(async () => {
145
+ try {
146
+ await deviceAddCommand();
147
+ } catch (error) {
148
+ console.error(chalk.red(`\n✗ Device add failed: ${error.message}\n`));
149
+ process.exit(1);
150
+ }
151
+ });
152
+
153
+ device
154
+ .command('list')
155
+ .description('List all configured devices')
156
+ .action(async () => {
157
+ try {
158
+ await deviceListCommand();
159
+ } catch (error) {
160
+ console.error(chalk.red(`\n✗ Device list failed: ${error.message}\n`));
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ device
166
+ .command('remove <name>')
167
+ .description('Remove a device by name')
168
+ .action(async (name) => {
169
+ try {
170
+ await deviceRemoveCommand(name);
171
+ } catch (error) {
172
+ console.error(chalk.red(`\n✗ Device remove failed: ${error.message}\n`));
173
+ process.exit(1);
174
+ }
175
+ });
176
+
177
+ program
178
+ .command('logs [service]')
179
+ .description('Tail service logs (voice-app, api-server, or all)')
180
+ .action(async (service) => {
181
+ try {
182
+ await logsCommand(service);
183
+ } catch (error) {
184
+ console.error(chalk.red(`\n✗ Logs command failed: ${error.message}\n`));
185
+ process.exit(1);
186
+ }
187
+ });
188
+
189
+ // Config management subcommands
190
+ const config = program
191
+ .command('config')
192
+ .description('Manage configuration');
193
+
194
+ config
195
+ .command('show')
196
+ .description('Display configuration with redacted secrets')
197
+ .action(async () => {
198
+ try {
199
+ await configShowCommand();
200
+ } catch (error) {
201
+ console.error(chalk.red(`\n✗ Config show failed: ${error.message}\n`));
202
+ process.exit(1);
203
+ }
204
+ });
205
+
206
+ config
207
+ .command('path')
208
+ .description('Show configuration file location')
209
+ .action(async () => {
210
+ try {
211
+ await configPathCommand();
212
+ } catch (error) {
213
+ console.error(chalk.red(`\n✗ Config path failed: ${error.message}\n`));
214
+ process.exit(1);
215
+ }
216
+ });
217
+
218
+ config
219
+ .command('reset')
220
+ .description('Reset configuration (creates backup)')
221
+ .action(async () => {
222
+ try {
223
+ await configResetCommand();
224
+ } catch (error) {
225
+ console.error(chalk.red(`\n✗ Config reset failed: ${error.message}\n`));
226
+ process.exit(1);
227
+ }
228
+ });
229
+
230
+ program
231
+ .command('update')
232
+ .description('Update Claude Phone to latest version')
233
+ .action(async () => {
234
+ try {
235
+ await updateCommand();
236
+ } catch (error) {
237
+ console.error(chalk.red(`\n✗ Update failed: ${error.message}\n`));
238
+ process.exit(1);
239
+ }
240
+ });
241
+
242
+ program
243
+ .command('backup')
244
+ .description('Create timestamped backup of configuration')
245
+ .action(async () => {
246
+ try {
247
+ await backupCommand();
248
+ } catch (error) {
249
+ console.error(chalk.red(`\n✗ Backup failed: ${error.message}\n`));
250
+ process.exit(1);
251
+ }
252
+ });
253
+
254
+ program
255
+ .command('restore')
256
+ .description('Restore configuration from backup')
257
+ .action(async () => {
258
+ try {
259
+ await restoreCommand();
260
+ } catch (error) {
261
+ console.error(chalk.red(`\n✗ Restore failed: ${error.message}\n`));
262
+ process.exit(1);
263
+ }
264
+ });
265
+
266
+ program
267
+ .command('uninstall')
268
+ .description('Uninstall Claude Phone completely')
269
+ .action(async () => {
270
+ try {
271
+ await uninstallCommand();
272
+ } catch (error) {
273
+ console.error(chalk.red(`\n✗ Uninstall failed: ${error.message}\n`));
274
+ process.exit(1);
275
+ }
276
+ });
277
+
278
+ // Parse arguments
279
+ program.parse(process.argv);
280
+
281
+ // Show help if no command
282
+ if (!process.argv.slice(2).length) {
283
+ program.outputHelp();
284
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Friendly next-steps message after `npm install -g claude-phone-local`.
3
+ // Never fails the install (package.json calls this with `|| true`).
4
+
5
+ console.log(`
6
+ \x1b[36m claude-phone-local installed \x1b[0m
7
+
8
+ Next steps:
9
+ 1. claude-phone setup # answers 3CX + local STT/TTS questions
10
+ 2. claude-phone doctor # checks Docker, Claude CLI, ports
11
+ 3. claude-phone start # builds + launches everything
12
+
13
+ Setup also registers the MCP server, so Claude Code can call YOU:
14
+ "call me when the build finishes"
15
+ (re-run later with: claude-phone mcp install)
16
+
17
+ Needs on this machine: Docker (Desktop or Engine) and Claude Code CLI
18
+ already logged in. Speech models and voices download automatically on
19
+ first run - no manual steps. See README-LOCAL-MODE.md for Windows/3CX.
20
+ `);
@@ -0,0 +1,77 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { spawn } from 'child_process';
4
+ import path from 'path';
5
+ import { loadConfig, configExists } from '../config.js';
6
+ import { getProjectRoot } from '../utils.js';
7
+ import { savePid, removePid } from '../process-manager.js';
8
+
9
+ /**
10
+ * API Server command - Start claude-api-server for remote connections
11
+ * @param {object} options - Command options
12
+ * @param {number} options.port - Port to listen on (default: 3333)
13
+ * @returns {Promise<void>}
14
+ */
15
+ export async function apiServerCommand(options = {}) {
16
+ console.log(chalk.bold.cyan('\n🤖 Claude API Server\n'));
17
+
18
+ // Load config to get port if not provided
19
+ let port = options.port;
20
+ if (!port && configExists()) {
21
+ const config = await loadConfig();
22
+ port = config.server?.claudeApiPort || 3333;
23
+ }
24
+ if (!port) {
25
+ port = 3333; // Final fallback
26
+ }
27
+
28
+ console.log(chalk.gray(`Starting API server on port ${port}...`));
29
+ console.log(chalk.gray('This wraps Claude Code CLI for Pi connections.\n'));
30
+
31
+ const projectRoot = getProjectRoot();
32
+ const serverPath = path.join(projectRoot, 'claude-api-server', 'server.js');
33
+
34
+ const spinner = ora('Starting server...').start();
35
+
36
+ try {
37
+ const child = spawn('node', [serverPath], {
38
+ env: {
39
+ ...process.env,
40
+ PORT: port.toString()
41
+ },
42
+ stdio: 'inherit'
43
+ });
44
+
45
+ // Save PID
46
+ savePid('claude-api-server', child.pid);
47
+
48
+ spinner.succeed(chalk.green('Server started'));
49
+ console.log(chalk.bold.cyan(`\n📡 Listening on port ${port}\n`));
50
+ console.log(chalk.gray('Waiting for Pi connections...'));
51
+ console.log(chalk.gray('Press Ctrl+C to stop\n'));
52
+
53
+ // Handle cleanup on exit
54
+ const cleanup = () => {
55
+ removePid('claude-api-server');
56
+ child.kill();
57
+ process.exit(0);
58
+ };
59
+
60
+ process.on('SIGINT', cleanup);
61
+ process.on('SIGTERM', cleanup);
62
+
63
+ // Wait for child to exit
64
+ child.on('exit', (code) => {
65
+ removePid('claude-api-server');
66
+ if (code !== 0) {
67
+ console.log(chalk.red(`\n✗ Server exited with code ${code}\n`));
68
+ process.exit(code);
69
+ }
70
+ });
71
+
72
+ } catch (error) {
73
+ spinner.fail(chalk.red('Failed to start server'));
74
+ console.error(chalk.red(`Error: ${error.message}\n`));
75
+ throw error;
76
+ }
77
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { getConfigPath, getConfigDir } from '../config.js';
6
+
7
+ /**
8
+ * Get the backups directory path
9
+ * @returns {string} Path to ~/.claude-phone/backups
10
+ */
11
+ function getBackupsDir() {
12
+ return path.join(getConfigDir(), 'backups');
13
+ }
14
+
15
+ /**
16
+ * Backup command - Create timestamped backup of configuration
17
+ * @returns {Promise<void>}
18
+ */
19
+ export async function backupCommand() {
20
+ console.log(chalk.bold.cyan('\n💾 Backing up configuration\n'));
21
+
22
+ const configPath = getConfigPath();
23
+
24
+ // Check if config exists
25
+ if (!fs.existsSync(configPath)) {
26
+ console.log(chalk.yellow('⚠️ No configuration found to backup.'));
27
+ console.log(chalk.gray('Run "claude-phone setup" to create configuration.\n'));
28
+ return;
29
+ }
30
+
31
+ // Create backups directory if it doesn't exist
32
+ const backupsDir = getBackupsDir();
33
+ if (!fs.existsSync(backupsDir)) {
34
+ await fs.promises.mkdir(backupsDir, { recursive: true, mode: 0o700 });
35
+ }
36
+
37
+ // Generate timestamp for filename
38
+ const now = new Date();
39
+ const timestamp = now.toISOString()
40
+ .replace(/T/, '.')
41
+ .replace(/:/g, '')
42
+ .replace(/\..+/, '');
43
+ const backupFilename = `config.${timestamp}.json`;
44
+ const backupPath = path.join(backupsDir, backupFilename);
45
+
46
+ // Copy config to backup
47
+ const spinner = ora('Creating backup...').start();
48
+ try {
49
+ await fs.promises.copyFile(configPath, backupPath);
50
+
51
+ // Get file size
52
+ const stats = await fs.promises.stat(backupPath);
53
+ const sizeMB = (stats.size / 1024).toFixed(2);
54
+
55
+ spinner.succeed('Backup created');
56
+
57
+ // Count existing backups
58
+ const backups = await fs.promises.readdir(backupsDir);
59
+ const configBackups = backups.filter(f => f.startsWith('config.') && f.endsWith('.json'));
60
+
61
+ console.log(chalk.bold.green('\n✓ Configuration backed up successfully!\n'));
62
+ console.log(chalk.gray(`Location: ${backupPath}`));
63
+ console.log(chalk.gray(`Size: ${sizeMB} KB`));
64
+ console.log(chalk.gray(`Total backups: ${configBackups.length}\n`));
65
+ } catch (error) {
66
+ spinner.fail(`Backup failed: ${error.message}`);
67
+ throw error;
68
+ }
69
+ }
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import { getConfigPath, getConfigDir, configExists } from '../../config.js';
3
+
4
+ /**
5
+ * Config path command - Show configuration file location
6
+ * @returns {Promise<void>}
7
+ */
8
+ export async function configPathCommand() {
9
+ console.log(chalk.bold.cyan('\n📂 Configuration Location\n'));
10
+
11
+ const configPath = getConfigPath();
12
+ const configDir = getConfigDir();
13
+
14
+ if (!configExists()) {
15
+ console.log(chalk.red('✗ Configuration not found'));
16
+ console.log(chalk.gray(` Expected location: ${configPath}\n`));
17
+ console.log(chalk.gray(' Run "claude-phone setup" to create configuration\n'));
18
+ return;
19
+ }
20
+
21
+ console.log(chalk.gray('Config directory:'));
22
+ console.log(chalk.bold(` ${configDir}`));
23
+ console.log();
24
+ console.log(chalk.gray('Config file:'));
25
+ console.log(chalk.bold(` ${configPath}`));
26
+ console.log();
27
+
28
+ console.log(chalk.yellow('⚠️ Warning: This file contains API keys and passwords'));
29
+ console.log(chalk.gray(' Do not share or commit this file to version control\n'));
30
+ }
@@ -0,0 +1,73 @@
1
+ import fs from 'fs';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { getConfigPath, getConfigDir, configExists } from '../../config.js';
5
+
6
+ /**
7
+ * Config reset command - Reset configuration with confirmation and backup
8
+ * @returns {Promise<void>}
9
+ */
10
+ export async function configResetCommand() {
11
+ console.log(chalk.bold.cyan('\n🔄 Reset Configuration\n'));
12
+
13
+ // Check if config exists
14
+ if (!configExists()) {
15
+ console.log(chalk.yellow('⚠️ No configuration found to reset\n'));
16
+ return;
17
+ }
18
+
19
+ const configPath = getConfigPath();
20
+ const configDir = getConfigDir();
21
+
22
+ // Confirmation prompt
23
+ console.log(chalk.yellow('⚠️ This will delete your current configuration:'));
24
+ console.log(chalk.gray(` ${configPath}\n`));
25
+ console.log(chalk.gray('A backup will be created before deletion.\n'));
26
+
27
+ const { confirmed } = await inquirer.prompt([
28
+ {
29
+ type: 'confirm',
30
+ name: 'confirmed',
31
+ message: 'Are you sure you want to reset?',
32
+ default: false
33
+ }
34
+ ]);
35
+
36
+ if (!confirmed) {
37
+ console.log(chalk.gray('\n✗ Reset cancelled\n'));
38
+ return;
39
+ }
40
+
41
+ try {
42
+ // Create backup with timestamp
43
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
44
+ const backupPath = `${configPath}.backup-${timestamp}`;
45
+ await fs.promises.copyFile(configPath, backupPath);
46
+ console.log(chalk.green(`\n✓ Backup created: ${backupPath}`));
47
+
48
+ // Delete config file
49
+ await fs.promises.unlink(configPath);
50
+ console.log(chalk.green('✓ Configuration deleted'));
51
+
52
+ // Clean up other generated files
53
+ const filesToClean = [
54
+ 'docker-compose.yml',
55
+ '.env',
56
+ 'server.pid'
57
+ ];
58
+
59
+ for (const file of filesToClean) {
60
+ const filePath = `${configDir}/${file}`;
61
+ if (fs.existsSync(filePath)) {
62
+ await fs.promises.unlink(filePath);
63
+ console.log(chalk.gray(` Removed: ${file}`));
64
+ }
65
+ }
66
+
67
+ console.log(chalk.bold.green('\n✓ Configuration reset complete'));
68
+ console.log(chalk.gray(' Run "claude-phone setup" to configure again\n'));
69
+ } catch (error) {
70
+ console.log(chalk.red(`\n✗ Reset failed: ${error.message}\n`));
71
+ throw error;
72
+ }
73
+ }
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, configExists } from '../../config.js';
3
+
4
+ /**
5
+ * Redact sensitive values for display
6
+ * @param {string} value - Value to redact
7
+ * @returns {string} Redacted value
8
+ */
9
+ function redactValue(value) {
10
+ if (!value || typeof value !== 'string') {
11
+ return '[not set]';
12
+ }
13
+
14
+ // For API keys (typically long strings)
15
+ if (value.length > 20) {
16
+ const prefix = value.slice(0, 4);
17
+ const suffix = value.slice(-4);
18
+ return `${prefix}...${suffix}`;
19
+ }
20
+
21
+ // For passwords and shorter secrets
22
+ return '••••••••';
23
+ }
24
+
25
+ /**
26
+ * Config show command - Display configuration with redacted secrets
27
+ * @returns {Promise<void>}
28
+ */
29
+ export async function configShowCommand() {
30
+ console.log(chalk.bold.cyan('\n⚙️ Claude Phone Configuration\n'));
31
+
32
+ // Check if configured
33
+ if (!configExists()) {
34
+ console.log(chalk.red('✗ Configuration not found'));
35
+ console.log(chalk.gray(' Run "claude-phone setup" first\n'));
36
+ return;
37
+ }
38
+
39
+ const config = await loadConfig();
40
+
41
+ // Speech-to-text / text-to-speech
42
+ console.log(chalk.bold('STT/TTS:'));
43
+ if (config.api && config.api.mode === 'local') {
44
+ console.log(chalk.gray(' Mode: local (no API keys — faster-whisper + Piper)'));
45
+ console.log(chalk.gray(` STT URL: ${config.api.local?.sttUrl || 'http://127.0.0.1:9001'}`));
46
+ console.log(chalk.gray(` TTS URL: ${config.api.local?.ttsUrl || 'http://127.0.0.1:9002'}`));
47
+ console.log(chalk.gray(` Whisper model: ${config.api.local?.whisperModel || 'small'}`));
48
+ } else {
49
+ console.log(chalk.gray(' Mode: cloud'));
50
+ console.log(chalk.gray(` OpenAI API Key: ${redactValue(config.api?.openai?.apiKey)}`));
51
+ console.log(chalk.gray(` ElevenLabs API Key: ${redactValue(config.api?.elevenlabs?.apiKey)}`));
52
+ }
53
+
54
+ // 3CX Configuration
55
+ console.log(chalk.bold('\n3CX Configuration:'));
56
+ console.log(chalk.gray(` SIP Domain: ${config.sip.domain}`));
57
+ console.log(chalk.gray(` SIP Registrar: ${config.sip.registrar}`));
58
+
59
+ // Server Configuration
60
+ console.log(chalk.bold('\nServer:'));
61
+ console.log(chalk.gray(` External IP: ${config.server.externalIp}`));
62
+ console.log(chalk.gray(` Voice App Port: ${config.server.voiceAppPort}`));
63
+ console.log(chalk.gray(` Claude API Port: ${config.server.claudeApiPort}`));
64
+ console.log(chalk.gray(` Claude API URL: ${config.server.claudeApiUrl}`));
65
+
66
+ // Devices
67
+ console.log(chalk.bold('\nDevices:'));
68
+ if (config.devices.length === 0) {
69
+ console.log(chalk.gray(' (none configured)'));
70
+ } else {
71
+ for (const device of config.devices) {
72
+ console.log(chalk.gray(` • ${device.name} (extension ${device.extension})`));
73
+ console.log(chalk.gray(` Auth ID: ${device.authId}`));
74
+ console.log(chalk.gray(` Password: ${redactValue(device.password)}`));
75
+ console.log(chalk.gray(` Voice ID: ${device.voiceId}`));
76
+ if (device.prompt) {
77
+ const shortPrompt = device.prompt.length > 50
78
+ ? device.prompt.slice(0, 50) + '...'
79
+ : device.prompt;
80
+ console.log(chalk.gray(` Prompt: ${shortPrompt}`));
81
+ }
82
+ }
83
+ }
84
+
85
+ console.log(chalk.gray(`\n💡 To view the raw config file, run: claude-phone config path\n`));
86
+ }