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