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,183 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import inquirer from 'inquirer';
7
+ import { getConfigDir, configExists } from '../config.js';
8
+ import { stopContainers } from '../docker.js';
9
+ import { stopServer, isServerRunning } from '../process-manager.js';
10
+
11
+ /**
12
+ * Get the CLI install directory path
13
+ * @returns {string} Path to ~/.claude-phone-cli
14
+ */
15
+ function getCliInstallDir() {
16
+ return path.join(os.homedir(), '.claude-phone-cli');
17
+ }
18
+
19
+ /**
20
+ * Find symlink location
21
+ * @returns {Promise<string|null>} Symlink path or null if not found
22
+ */
23
+ async function findSymlink() {
24
+ const possiblePaths = [
25
+ '/usr/local/bin/claude-phone',
26
+ path.join(os.homedir(), '.local/bin/claude-phone')
27
+ ];
28
+
29
+ for (const symlinkPath of possiblePaths) {
30
+ try {
31
+ const stats = await fs.promises.lstat(symlinkPath);
32
+ if (stats.isSymbolicLink()) {
33
+ return symlinkPath;
34
+ }
35
+ } catch (error) {
36
+ // Path doesn't exist, continue
37
+ }
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Remove directory recursively
45
+ * @param {string} dirPath - Directory path to remove
46
+ * @returns {Promise<void>}
47
+ */
48
+ async function removeDirectory(dirPath) {
49
+ if (fs.existsSync(dirPath)) {
50
+ await fs.promises.rm(dirPath, { recursive: true, force: true });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Uninstall command - Complete removal of Claude Phone
56
+ * @returns {Promise<void>}
57
+ */
58
+ export async function uninstallCommand() {
59
+ console.log(chalk.bold.red('\n🗑️ Uninstall Claude Phone\n'));
60
+
61
+ const configDir = getConfigDir();
62
+ const cliDir = getCliInstallDir();
63
+ const hasConfig = configExists();
64
+ const symlinkPath = await findSymlink();
65
+
66
+ // Show what will be removed
67
+ console.log(chalk.bold('The following will be removed:\n'));
68
+
69
+ if (hasConfig || fs.existsSync(configDir)) {
70
+ console.log(chalk.yellow(' • Configuration directory:'));
71
+ console.log(chalk.gray(` ${configDir}`));
72
+ console.log(chalk.gray(' (includes config.json, backups, generated files)'));
73
+ }
74
+
75
+ if (fs.existsSync(cliDir)) {
76
+ console.log(chalk.yellow('\n • CLI installation:'));
77
+ console.log(chalk.gray(` ${cliDir}`));
78
+ }
79
+
80
+ if (symlinkPath) {
81
+ console.log(chalk.yellow('\n • Symlink:'));
82
+ console.log(chalk.gray(` ${symlinkPath}`));
83
+ }
84
+
85
+ console.log(chalk.yellow('\n • Docker containers:'));
86
+ console.log(chalk.gray(' voice-app, drachtio, freeswitch'));
87
+
88
+ // First confirmation: Remove Claude Phone?
89
+ console.log(chalk.bold.red('\n⚠️ WARNING: This action cannot be undone!\n'));
90
+
91
+ const { confirmUninstall } = await inquirer.prompt([
92
+ {
93
+ type: 'confirm',
94
+ name: 'confirmUninstall',
95
+ message: 'Remove Claude Phone?',
96
+ default: false
97
+ }
98
+ ]);
99
+
100
+ if (!confirmUninstall) {
101
+ console.log(chalk.gray('\nUninstall cancelled.\n'));
102
+ return;
103
+ }
104
+
105
+ // Second confirmation: Delete configuration and backups?
106
+ if (hasConfig || fs.existsSync(configDir)) {
107
+ const { confirmConfig } = await inquirer.prompt([
108
+ {
109
+ type: 'confirm',
110
+ name: 'confirmConfig',
111
+ message: 'Delete configuration and backups? (This will remove API keys and all backups)',
112
+ default: false
113
+ }
114
+ ]);
115
+
116
+ if (!confirmConfig) {
117
+ console.log(chalk.gray('\nUninstall cancelled. Configuration preserved.\n'));
118
+ return;
119
+ }
120
+ }
121
+
122
+ console.log(chalk.bold.cyan('\n🧹 Removing Claude Phone...\n'));
123
+
124
+ // Step 1: Stop services
125
+ let spinner = ora('Stopping Claude API server...').start();
126
+ try {
127
+ if (await isServerRunning()) {
128
+ await stopServer();
129
+ spinner.succeed('Claude API server stopped');
130
+ } else {
131
+ spinner.info('Claude API server not running');
132
+ }
133
+ } catch (error) {
134
+ spinner.warn(`Could not stop server: ${error.message}`);
135
+ }
136
+
137
+ spinner = ora('Stopping Docker containers...').start();
138
+ try {
139
+ await stopContainers();
140
+ spinner.succeed('Docker containers stopped');
141
+ } catch (error) {
142
+ spinner.warn(`Could not stop containers: ${error.message}`);
143
+ }
144
+
145
+ // Step 2: Remove configuration directory
146
+ if (fs.existsSync(configDir)) {
147
+ spinner = ora('Removing configuration directory...').start();
148
+ try {
149
+ await removeDirectory(configDir);
150
+ spinner.succeed('Configuration directory removed');
151
+ } catch (error) {
152
+ spinner.fail(`Failed to remove config directory: ${error.message}`);
153
+ }
154
+ }
155
+
156
+ // Step 3: Remove CLI installation
157
+ if (fs.existsSync(cliDir)) {
158
+ spinner = ora('Removing CLI installation...').start();
159
+ try {
160
+ await removeDirectory(cliDir);
161
+ spinner.succeed('CLI installation removed');
162
+ } catch (error) {
163
+ spinner.fail(`Failed to remove CLI directory: ${error.message}`);
164
+ }
165
+ }
166
+
167
+ // Step 4: Remove symlink
168
+ if (symlinkPath) {
169
+ spinner = ora('Removing symlink...').start();
170
+ try {
171
+ await fs.promises.unlink(symlinkPath);
172
+ spinner.succeed('Symlink removed');
173
+ } catch (error) {
174
+ spinner.warn(`Could not remove symlink: ${error.message}`);
175
+ }
176
+ }
177
+
178
+ console.log(chalk.bold.green('\n✓ Claude Phone uninstalled\n'));
179
+ console.log(chalk.gray('Thank you for using Claude Phone! 👋\n'));
180
+ console.log(chalk.gray('Note: Docker images were not removed. To clean them up, run:'));
181
+ console.log(chalk.gray(' docker image rm drachtio/drachtio-server:latest'));
182
+ console.log(chalk.gray(' docker image rm drachtio/drachtio-freeswitch-mrf:latest\n'));
183
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import chalk from 'chalk';
5
+ import inquirer from 'inquirer';
6
+ import { execSync } from 'child_process';
7
+ import { loadConfig, saveConfig, configExists } from '../config.js';
8
+
9
+ /**
10
+ * Check for git repository
11
+ * @param {string} projectRoot - Root directory of claude-phone
12
+ * @returns {boolean} True if git repo
13
+ */
14
+ function isGitRepo(projectRoot) {
15
+ try {
16
+ execSync('git rev-parse --is-inside-work-tree', {
17
+ cwd: projectRoot,
18
+ stdio: 'ignore'
19
+ });
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get current git branch
28
+ * @param {string} projectRoot - Root directory of claude-phone
29
+ * @returns {string} Branch name
30
+ */
31
+ function getCurrentBranch(projectRoot) {
32
+ try {
33
+ return execSync('git rev-parse --abbrev-ref HEAD', {
34
+ cwd: projectRoot,
35
+ encoding: 'utf8'
36
+ }).trim();
37
+ } catch {
38
+ return 'unknown';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get project root directory (where package.json is)
44
+ * @returns {string} Project root path
45
+ */
46
+ function getProjectRoot() {
47
+ // CLI is in cli/lib/commands/, project root is three levels up
48
+ const currentFile = fileURLToPath(import.meta.url);
49
+ const commandsDir = path.dirname(currentFile);
50
+ const libDir = path.dirname(commandsDir);
51
+ const cliDir = path.dirname(libDir);
52
+ const projectRoot = path.dirname(cliDir);
53
+ return fs.realpathSync(projectRoot);
54
+ }
55
+
56
+ /**
57
+ * Fetch latest release info from GitHub
58
+ * @returns {Promise<object>} Release info
59
+ */
60
+ async function fetchLatestRelease() {
61
+ try {
62
+ const response = await fetch(
63
+ 'https://api.github.com/repos/masterdeepak15/claude-phone-local/releases/latest',
64
+ {
65
+ headers: {
66
+ 'User-Agent': 'claude-phone-cli',
67
+ 'Accept': 'application/vnd.github.v3+json'
68
+ }
69
+ }
70
+ );
71
+
72
+ if (!response.ok) {
73
+ throw new Error(`GitHub API error: ${response.statusText}`);
74
+ }
75
+
76
+ return await response.json();
77
+ } catch (error) {
78
+ throw new Error(`Failed to fetch latest release: ${error.message}`);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Update via git pull
84
+ * @param {string} projectRoot - Root directory
85
+ * @returns {Promise<void>}
86
+ */
87
+ async function updateViaGit(projectRoot) {
88
+ console.log(chalk.bold('\nUpdating from git...\n'));
89
+
90
+ try {
91
+ // Check current branch
92
+ const branch = getCurrentBranch(projectRoot);
93
+ console.log(chalk.gray(`Current branch: ${branch}`));
94
+
95
+ // Fetch latest
96
+ console.log(chalk.gray('Fetching latest changes...'));
97
+ execSync('git fetch origin', {
98
+ cwd: projectRoot,
99
+ stdio: 'inherit'
100
+ });
101
+
102
+ // Check for uncommitted changes
103
+ const status = execSync('git status --porcelain', {
104
+ cwd: projectRoot,
105
+ encoding: 'utf8'
106
+ });
107
+
108
+ if (status.trim()) {
109
+ console.log(chalk.yellow('\n⚠️ You have uncommitted changes:'));
110
+ console.log(chalk.gray(status));
111
+ const { proceed } = await inquirer.prompt([
112
+ {
113
+ type: 'confirm',
114
+ name: 'proceed',
115
+ message: 'Continue with update? (changes will be stashed)',
116
+ default: false
117
+ }
118
+ ]);
119
+
120
+ if (!proceed) {
121
+ console.log(chalk.gray('\n✗ Update cancelled\n'));
122
+ return;
123
+ }
124
+
125
+ // Stash changes
126
+ execSync('git stash', { cwd: projectRoot, stdio: 'inherit' });
127
+ console.log(chalk.green('✓ Changes stashed'));
128
+ }
129
+
130
+ // Pull latest
131
+ console.log(chalk.gray('\nPulling latest changes...'));
132
+ execSync('git pull origin main', {
133
+ cwd: projectRoot,
134
+ stdio: 'inherit'
135
+ });
136
+
137
+ console.log(chalk.green('\n✓ Update complete\n'));
138
+ console.log(chalk.gray('Run "claude-phone status" to verify services\n'));
139
+ } catch (error) {
140
+ throw new Error(`Git update failed: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Show manual update instructions
146
+ * @param {object} release - Latest release info
147
+ * @returns {void}
148
+ */
149
+ function showManualInstructions(release) {
150
+ console.log(chalk.bold('\n📥 Manual Update Instructions\n'));
151
+ console.log(chalk.gray('Latest version:'), chalk.bold(release.tag_name));
152
+ console.log(chalk.gray('Released:'), release.published_at.split('T')[0]);
153
+ console.log();
154
+
155
+ console.log(chalk.bold('To update manually:\n'));
156
+ console.log(chalk.gray('1. Stop all services:'));
157
+ console.log(chalk.bold(' claude-phone stop\n'));
158
+
159
+ console.log(chalk.gray('2. Backup your configuration:'));
160
+ console.log(chalk.bold(' cp ~/.claude-phone/config.json ~/config.json.backup\n'));
161
+
162
+ console.log(chalk.gray('3. Run the installer:'));
163
+ console.log(chalk.bold(' npm install -g claude-phone-local\n'));
164
+
165
+ console.log(chalk.gray('4. Start services:'));
166
+ console.log(chalk.bold(' claude-phone start\n'));
167
+
168
+ console.log(chalk.yellow('⚠️ Your configuration will be preserved automatically\n'));
169
+ }
170
+
171
+ /**
172
+ * Update command - Update Claude Phone to latest version
173
+ * @returns {Promise<void>}
174
+ */
175
+ export async function updateCommand() {
176
+ console.log(chalk.bold.cyan('\n🔄 Update Claude Phone\n'));
177
+
178
+ const projectRoot = getProjectRoot();
179
+
180
+ // Backup config before update
181
+ if (configExists()) {
182
+ console.log(chalk.gray('Backing up configuration...'));
183
+ const config = await loadConfig();
184
+ const backupPath = `${process.env.HOME}/.claude-phone/config.json.pre-update`;
185
+ await saveConfig(config); // This creates a backup automatically
186
+ console.log(chalk.green(`✓ Config backed up to: ${backupPath}`));
187
+ }
188
+
189
+ // Check if git repo
190
+ if (isGitRepo(projectRoot)) {
191
+ console.log(chalk.gray('Detected git installation\n'));
192
+ await updateViaGit(projectRoot);
193
+ } else {
194
+ // Non-git installation - show manual instructions
195
+ console.log(chalk.gray('Checking for latest release...\n'));
196
+
197
+ try {
198
+ const release = await fetchLatestRelease();
199
+ showManualInstructions(release);
200
+ } catch (error) {
201
+ console.log(chalk.red(`\n✗ ${error.message}\n`));
202
+ console.log(chalk.gray('Visit https://github.com/masterdeepak15/claude-phone-local for manual update\n'));
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,113 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ /**
6
+ * Get the config directory path
7
+ * @returns {string} Path to ~/.claude-phone
8
+ */
9
+ export function getConfigDir() {
10
+ return path.join(os.homedir(), '.claude-phone');
11
+ }
12
+
13
+ /**
14
+ * Get the config file path
15
+ * @returns {string} Path to ~/.claude-phone/config.json
16
+ */
17
+ export function getConfigPath() {
18
+ return path.join(getConfigDir(), 'config.json');
19
+ }
20
+
21
+ /**
22
+ * Check if config file exists
23
+ * @returns {boolean} True if config exists
24
+ */
25
+ export function configExists() {
26
+ return fs.existsSync(getConfigPath());
27
+ }
28
+
29
+ /**
30
+ * Load configuration from disk
31
+ * @returns {Promise<object>} Configuration object
32
+ */
33
+ export async function loadConfig() {
34
+ const configPath = getConfigPath();
35
+
36
+ if (!fs.existsSync(configPath)) {
37
+ throw new Error('Configuration not found. Run "claude-phone setup" first.');
38
+ }
39
+
40
+ const data = await fs.promises.readFile(configPath, 'utf8');
41
+ const config = JSON.parse(data);
42
+
43
+ // Ensure installationType exists for backward compatibility
44
+ if (!config.installationType) {
45
+ config.installationType = 'both';
46
+ }
47
+
48
+ return config;
49
+ }
50
+
51
+ /**
52
+ * Get the installation type from config
53
+ * @param {object} config - Configuration object
54
+ * @returns {string} Installation type ('voice-server' | 'api-server' | 'both')
55
+ */
56
+ export function getInstallationType(config) {
57
+ return config.installationType || 'both';
58
+ }
59
+
60
+ /**
61
+ * Save configuration to disk
62
+ * @param {object} config - Configuration object
63
+ * @returns {Promise<void>}
64
+ */
65
+ export async function saveConfig(config) {
66
+ const configDir = getConfigDir();
67
+ const configPath = getConfigPath();
68
+
69
+ // Create directory if it doesn't exist
70
+ if (!fs.existsSync(configDir)) {
71
+ await fs.promises.mkdir(configDir, { recursive: true, mode: 0o700 });
72
+ }
73
+
74
+ // Backup existing config if it exists
75
+ if (fs.existsSync(configPath)) {
76
+ const backupPath = configPath + '.backup';
77
+ await fs.promises.copyFile(configPath, backupPath);
78
+ }
79
+
80
+ // Add security warning to config
81
+ const configWithWarning = {
82
+ _WARNING: 'DO NOT SHARE THIS FILE - Contains API keys and passwords',
83
+ ...config
84
+ };
85
+
86
+ // Write config file
87
+ const data = JSON.stringify(configWithWarning, null, 2);
88
+ await fs.promises.writeFile(configPath, data, { mode: 0o600 });
89
+ }
90
+
91
+ /**
92
+ * Get the PID file path
93
+ * @returns {string} Path to server.pid
94
+ */
95
+ export function getPidPath() {
96
+ return path.join(getConfigDir(), 'server.pid');
97
+ }
98
+
99
+ /**
100
+ * Get the docker-compose.yml path
101
+ * @returns {string} Path to generated docker-compose.yml
102
+ */
103
+ export function getDockerComposePath() {
104
+ return path.join(getConfigDir(), 'docker-compose.yml');
105
+ }
106
+
107
+ /**
108
+ * Get the .env file path
109
+ * @returns {string} Path to generated .env
110
+ */
111
+ export function getEnvPath() {
112
+ return path.join(getConfigDir(), '.env');
113
+ }