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,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
|
+
}
|