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,186 @@
1
+ import chalk from 'chalk';
2
+ import { spawn } from 'child_process';
3
+ import axios from 'axios';
4
+ import { loadConfig, configExists, getDockerComposePath, getPidPath } from '../config.js';
5
+ import fs from 'fs';
6
+
7
+ /**
8
+ * Logs command - Tail service logs
9
+ * @param {string} [service] - Optional service name (voice-app or api-server)
10
+ * @returns {Promise<void>}
11
+ */
12
+ export async function logsCommand(service = null) {
13
+ if (!configExists()) {
14
+ console.log(chalk.red('\n✗ Not configured'));
15
+ console.log(chalk.gray(' Run "claude-phone setup" first\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const config = await loadConfig();
20
+ const dockerComposePath = getDockerComposePath();
21
+
22
+ // Validate service argument
23
+ const validServices = ['voice-app', 'api-server'];
24
+ if (service && !validServices.includes(service)) {
25
+ console.log(chalk.red(`\n✗ Invalid service: ${service}`));
26
+ console.log(chalk.gray(' Valid services: voice-app, api-server'));
27
+ console.log(chalk.gray(' Or omit service to tail all logs\n'));
28
+ process.exit(1);
29
+ }
30
+
31
+ // Header
32
+ if (service) {
33
+ console.log(chalk.bold.cyan(`\n📋 Tailing logs for ${service}...\n`));
34
+ } else {
35
+ console.log(chalk.bold.cyan('\n📋 Tailing all service logs...\n'));
36
+ }
37
+
38
+ // Handle different service options
39
+ if (!service || service === 'voice-app') {
40
+ // Docker container logs
41
+ if (!fs.existsSync(dockerComposePath)) {
42
+ console.log(chalk.yellow('⚠ Docker containers not configured'));
43
+ console.log(chalk.gray(' Run "claude-phone start" first\n'));
44
+ if (service === 'voice-app') {
45
+ process.exit(1);
46
+ }
47
+ } else if (!service) {
48
+ // Both services - interleave logs
49
+ tailBothServices(dockerComposePath, config);
50
+ return;
51
+ } else {
52
+ // Just voice-app
53
+ tailDockerLogs(dockerComposePath);
54
+ return;
55
+ }
56
+ }
57
+
58
+ if (!service || service === 'api-server') {
59
+ // API server logs
60
+ const pidPath = getPidPath();
61
+ if (!fs.existsSync(pidPath)) {
62
+ console.log(chalk.yellow('⚠ Claude API server not running'));
63
+ console.log(chalk.gray(' Run "claude-phone start" first\n'));
64
+ if (service === 'api-server') {
65
+ process.exit(1);
66
+ }
67
+ } else if (service === 'api-server') {
68
+ tailAPIServerLogs(config);
69
+ return;
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Tail Docker container logs
76
+ * @param {string} dockerComposePath - Path to docker-compose.yml
77
+ */
78
+ function tailDockerLogs(dockerComposePath) {
79
+ const child = spawn('docker', [
80
+ 'compose',
81
+ '-f',
82
+ dockerComposePath,
83
+ 'logs',
84
+ '-f',
85
+ '--tail=50'
86
+ ], {
87
+ stdio: 'inherit'
88
+ });
89
+
90
+ // Handle Ctrl+C gracefully
91
+ process.on('SIGINT', () => {
92
+ child.kill('SIGTERM');
93
+ console.log(chalk.gray('\n\nStopped tailing logs.\n'));
94
+ process.exit(0);
95
+ });
96
+
97
+ child.on('close', (code) => {
98
+ if (code !== 0 && code !== null) {
99
+ console.log(chalk.red(`\n✗ Docker logs failed with exit code ${code}\n`));
100
+ process.exit(code);
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Tail API server logs
107
+ * @param {object} config - Configuration object
108
+ */
109
+ function tailAPIServerLogs(config) {
110
+ console.log(chalk.gray('Watching Claude API server output...\n'));
111
+
112
+ // Since the server runs detached, we can't easily tail its logs
113
+ // Instead, we'll monitor its health endpoint
114
+ console.log(chalk.yellow('Note: API server logs are not available (runs detached)'));
115
+ console.log(chalk.gray('Monitoring health endpoint instead...\n'));
116
+
117
+ let consecutiveFailures = 0;
118
+
119
+ const checkHealth = async () => {
120
+ try {
121
+ const response = await axios.get(`http://localhost:${config.server.claudeApiPort}/health`, {
122
+ timeout: 3000
123
+ });
124
+
125
+ if (response.status === 200) {
126
+ console.log(chalk.green(`[${new Date().toISOString()}] ✓ API server healthy`));
127
+ consecutiveFailures = 0;
128
+ } else {
129
+ console.log(chalk.yellow(`[${new Date().toISOString()}] ⚠ Unexpected status: ${response.status}`));
130
+ }
131
+ } catch (error) {
132
+ consecutiveFailures++;
133
+ console.log(chalk.red(`[${new Date().toISOString()}] ✗ Health check failed: ${error.message}`));
134
+
135
+ if (consecutiveFailures >= 3) {
136
+ console.log(chalk.red('\n✗ API server appears to be down. Stopping health checks.\n'));
137
+ process.exit(1);
138
+ }
139
+ }
140
+ };
141
+
142
+ // Check immediately, then every 5 seconds
143
+ checkHealth();
144
+ const interval = setInterval(checkHealth, 5000);
145
+
146
+ // Handle Ctrl+C
147
+ process.on('SIGINT', () => {
148
+ clearInterval(interval);
149
+ console.log(chalk.gray('\n\nStopped monitoring API server.\n'));
150
+ process.exit(0);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Tail both services with interleaved output
156
+ * @param {string} dockerComposePath - Path to docker-compose.yml
157
+ * @param {object} _config - Configuration object (unused)
158
+ */
159
+ function tailBothServices(dockerComposePath, _config) {
160
+ console.log(chalk.gray('Showing Docker container logs (API server logs not available)\n'));
161
+
162
+ const child = spawn('docker', [
163
+ 'compose',
164
+ '-f',
165
+ dockerComposePath,
166
+ 'logs',
167
+ '-f',
168
+ '--tail=50'
169
+ ], {
170
+ stdio: 'inherit'
171
+ });
172
+
173
+ // Handle Ctrl+C gracefully
174
+ process.on('SIGINT', () => {
175
+ child.kill('SIGTERM');
176
+ console.log(chalk.gray('\n\nStopped tailing logs.\n'));
177
+ process.exit(0);
178
+ });
179
+
180
+ child.on('close', (code) => {
181
+ if (code !== 0 && code !== null) {
182
+ console.log(chalk.red(`\n✗ Docker logs failed with exit code ${code}\n`));
183
+ process.exit(code);
184
+ }
185
+ });
186
+ }
@@ -0,0 +1,172 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import inquirer from 'inquirer';
6
+ import { getConfigPath, getConfigDir } from '../config.js';
7
+
8
+ /**
9
+ * Get the backups directory path
10
+ * @returns {string} Path to ~/.claude-phone/backups
11
+ */
12
+ function getBackupsDir() {
13
+ return path.join(getConfigDir(), 'backups');
14
+ }
15
+
16
+ /**
17
+ * Format file size in human-readable format
18
+ * @param {number} bytes - File size in bytes
19
+ * @returns {string} Formatted size string
20
+ */
21
+ function formatSize(bytes) {
22
+ if (bytes < 1024) return `${bytes} B`;
23
+ const kb = bytes / 1024;
24
+ if (kb < 1024) return `${kb.toFixed(2)} KB`;
25
+ const mb = kb / 1024;
26
+ return `${mb.toFixed(2)} MB`;
27
+ }
28
+
29
+ /**
30
+ * Parse timestamp from backup filename
31
+ * @param {string} filename - Backup filename
32
+ * @returns {Date|null} Parsed date or null
33
+ */
34
+ function parseBackupTimestamp(filename) {
35
+ // Extract timestamp from format: config.YYYY-MM-DD.HHmmss.json
36
+ const match = filename.match(/config\.(\d{4}-\d{2}-\d{2})\.(\d{6})\.json/);
37
+ if (!match) return null;
38
+
39
+ const datePart = match[1];
40
+ const timePart = match[2];
41
+
42
+ // Parse: YYYY-MM-DD and HHmmss
43
+ const year = datePart.substring(0, 4);
44
+ const month = datePart.substring(5, 7);
45
+ const day = datePart.substring(8, 10);
46
+ const hour = timePart.substring(0, 2);
47
+ const minute = timePart.substring(2, 4);
48
+ const second = timePart.substring(4, 6);
49
+
50
+ return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`);
51
+ }
52
+
53
+ /**
54
+ * Format date in readable format
55
+ * @param {Date} date - Date object
56
+ * @returns {string} Formatted date string
57
+ */
58
+ function formatDate(date) {
59
+ return date.toLocaleString('en-US', {
60
+ year: 'numeric',
61
+ month: 'short',
62
+ day: 'numeric',
63
+ hour: '2-digit',
64
+ minute: '2-digit',
65
+ second: '2-digit'
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Restore command - Restore configuration from backup
71
+ * @returns {Promise<void>}
72
+ */
73
+ export async function restoreCommand() {
74
+ console.log(chalk.bold.cyan('\n🔄 Restore Configuration\n'));
75
+
76
+ const backupsDir = getBackupsDir();
77
+
78
+ // Check if backups directory exists
79
+ if (!fs.existsSync(backupsDir)) {
80
+ console.log(chalk.yellow('⚠️ No backups directory found.'));
81
+ console.log(chalk.gray('Create a backup first with: claude-phone backup\n'));
82
+ return;
83
+ }
84
+
85
+ // List available backups
86
+ const files = await fs.promises.readdir(backupsDir);
87
+ const backups = files.filter(f => f.startsWith('config.') && f.endsWith('.json'));
88
+
89
+ if (backups.length === 0) {
90
+ console.log(chalk.yellow('⚠️ No backups found.'));
91
+ console.log(chalk.gray('Create a backup first with: claude-phone backup\n'));
92
+ return;
93
+ }
94
+
95
+ // Sort backups by timestamp (newest first)
96
+ backups.sort().reverse();
97
+
98
+ // Get backup details
99
+ const backupChoices = await Promise.all(
100
+ backups.map(async (filename) => {
101
+ const filepath = path.join(backupsDir, filename);
102
+ const stats = await fs.promises.stat(filepath);
103
+ const date = parseBackupTimestamp(filename);
104
+ const dateStr = date ? formatDate(date) : 'Unknown date';
105
+ const size = formatSize(stats.size);
106
+
107
+ return {
108
+ name: `${dateStr} (${size})`,
109
+ value: filename,
110
+ short: filename
111
+ };
112
+ })
113
+ );
114
+
115
+ // Let user select backup
116
+ const { selectedBackup } = await inquirer.prompt([
117
+ {
118
+ type: 'list',
119
+ name: 'selectedBackup',
120
+ message: 'Select backup to restore:',
121
+ choices: backupChoices,
122
+ pageSize: 10
123
+ }
124
+ ]);
125
+
126
+ const backupPath = path.join(backupsDir, selectedBackup);
127
+
128
+ // Show what will be restored
129
+ console.log(chalk.bold('\n📋 Restore details:'));
130
+ const stats = await fs.promises.stat(backupPath);
131
+ console.log(chalk.gray(` Source: ${selectedBackup}`));
132
+ console.log(chalk.gray(` Size: ${formatSize(stats.size)}`));
133
+
134
+ // Confirm restoration
135
+ const { confirmed } = await inquirer.prompt([
136
+ {
137
+ type: 'confirm',
138
+ name: 'confirmed',
139
+ message: 'This will replace your current configuration. Continue?',
140
+ default: false
141
+ }
142
+ ]);
143
+
144
+ if (!confirmed) {
145
+ console.log(chalk.gray('\nRestore cancelled.\n'));
146
+ return;
147
+ }
148
+
149
+ const spinner = ora('Restoring configuration...').start();
150
+
151
+ try {
152
+ const configPath = getConfigPath();
153
+
154
+ // Backup current config before overwriting (safety)
155
+ if (fs.existsSync(configPath)) {
156
+ const safetyBackup = configPath + '.pre-restore';
157
+ await fs.promises.copyFile(configPath, safetyBackup);
158
+ spinner.text = 'Current config backed up, restoring...';
159
+ }
160
+
161
+ // Restore selected backup
162
+ await fs.promises.copyFile(backupPath, configPath);
163
+
164
+ spinner.succeed('Configuration restored');
165
+
166
+ console.log(chalk.bold.green('\n✓ Configuration restored successfully!\n'));
167
+ console.log(chalk.gray('Run "claude-phone start" to apply changes.\n'));
168
+ } catch (error) {
169
+ spinner.fail(`Restore failed: ${error.message}`);
170
+ throw error;
171
+ }
172
+ }