claude-voice 1.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +395 -0
  3. package/bin/claude-voice +29 -0
  4. package/config/default.json +109 -0
  5. package/config/voice-prompt.md +27 -0
  6. package/dist/cli.d.ts +8 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +1103 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +140 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +179 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/env.d.ts +40 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +175 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +140 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/platform/index.d.ts +35 -0
  23. package/dist/platform/index.d.ts.map +1 -0
  24. package/dist/platform/index.js +170 -0
  25. package/dist/platform/index.js.map +1 -0
  26. package/dist/server.d.ts +5 -0
  27. package/dist/server.d.ts.map +1 -0
  28. package/dist/server.js +185 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/stt/index.d.ts +15 -0
  31. package/dist/stt/index.d.ts.map +1 -0
  32. package/dist/stt/index.js +54 -0
  33. package/dist/stt/index.js.map +1 -0
  34. package/dist/stt/providers/openai.d.ts +15 -0
  35. package/dist/stt/providers/openai.d.ts.map +1 -0
  36. package/dist/stt/providers/openai.js +74 -0
  37. package/dist/stt/providers/openai.js.map +1 -0
  38. package/dist/stt/providers/sherpa-onnx.d.ts +50 -0
  39. package/dist/stt/providers/sherpa-onnx.d.ts.map +1 -0
  40. package/dist/stt/providers/sherpa-onnx.js +237 -0
  41. package/dist/stt/providers/sherpa-onnx.js.map +1 -0
  42. package/dist/stt/providers/whisper-local.d.ts +19 -0
  43. package/dist/stt/providers/whisper-local.d.ts.map +1 -0
  44. package/dist/stt/providers/whisper-local.js +141 -0
  45. package/dist/stt/providers/whisper-local.js.map +1 -0
  46. package/dist/terminal/input-injector.d.ts +55 -0
  47. package/dist/terminal/input-injector.d.ts.map +1 -0
  48. package/dist/terminal/input-injector.js +189 -0
  49. package/dist/terminal/input-injector.js.map +1 -0
  50. package/dist/tts/index.d.ts +20 -0
  51. package/dist/tts/index.d.ts.map +1 -0
  52. package/dist/tts/index.js +72 -0
  53. package/dist/tts/index.js.map +1 -0
  54. package/dist/tts/providers/elevenlabs.d.ts +23 -0
  55. package/dist/tts/providers/elevenlabs.d.ts.map +1 -0
  56. package/dist/tts/providers/elevenlabs.js +142 -0
  57. package/dist/tts/providers/elevenlabs.js.map +1 -0
  58. package/dist/tts/providers/macos-say.d.ts +17 -0
  59. package/dist/tts/providers/macos-say.d.ts.map +1 -0
  60. package/dist/tts/providers/macos-say.js +72 -0
  61. package/dist/tts/providers/macos-say.js.map +1 -0
  62. package/dist/tts/providers/openai.d.ts +19 -0
  63. package/dist/tts/providers/openai.d.ts.map +1 -0
  64. package/dist/tts/providers/openai.js +118 -0
  65. package/dist/tts/providers/openai.js.map +1 -0
  66. package/dist/tts/providers/piper.d.ts +48 -0
  67. package/dist/tts/providers/piper.d.ts.map +1 -0
  68. package/dist/tts/providers/piper.js +417 -0
  69. package/dist/tts/providers/piper.js.map +1 -0
  70. package/dist/voice-input.d.ts +9 -0
  71. package/dist/voice-input.d.ts.map +1 -0
  72. package/dist/voice-input.js +137 -0
  73. package/dist/voice-input.js.map +1 -0
  74. package/dist/wake-word/index.d.ts +19 -0
  75. package/dist/wake-word/index.d.ts.map +1 -0
  76. package/dist/wake-word/index.js +200 -0
  77. package/dist/wake-word/index.js.map +1 -0
  78. package/dist/wake-word/recorder.d.ts +19 -0
  79. package/dist/wake-word/recorder.d.ts.map +1 -0
  80. package/dist/wake-word/recorder.js +145 -0
  81. package/dist/wake-word/recorder.js.map +1 -0
  82. package/hooks/notification.js +125 -0
  83. package/hooks/post-tool-use.js +374 -0
  84. package/hooks/session-start.js +212 -0
  85. package/hooks/stop.js +254 -0
  86. package/models/.gitkeep +0 -0
  87. package/package.json +80 -0
  88. package/python/stt_service.py +59 -0
  89. package/python/voice_input.py +154 -0
  90. package/scripts/install.sh +147 -0
  91. package/scripts/listen.py +161 -0
  92. package/scripts/postinstall.js +57 -0
  93. package/scripts/record.sh +79 -0
  94. package/scripts/setup-hooks.sh +22 -0
  95. package/scripts/voice-input.sh +66 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1103 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Claude Voice Extension CLI
5
+ *
6
+ * Command-line interface for managing the voice extension.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ const commander_1 = require("commander");
43
+ const child_process_1 = require("child_process");
44
+ const http = __importStar(require("http"));
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const config_1 = require("./config");
48
+ const tts_1 = require("./tts");
49
+ const stt_1 = require("./stt");
50
+ const env_1 = require("./env");
51
+ const platform_1 = require("./platform");
52
+ const sherpa_onnx_1 = require("./stt/providers/sherpa-onnx");
53
+ const piper_1 = require("./tts/providers/piper");
54
+ const API_URL = 'http://127.0.0.1:3456';
55
+ const PID_FILE = path.join(process.env.HOME || '~', '.claude-voice', 'daemon.pid');
56
+ const LOG_FILE = path.join(process.env.HOME || '~', '.claude-voice', 'daemon.log');
57
+ const program = new commander_1.Command();
58
+ program.name('claude-voice').description('Voice interface extension for Claude Code').version('1.0.0');
59
+ // ============================================================================
60
+ // Helper Functions
61
+ // ============================================================================
62
+ function checkDaemon() {
63
+ return new Promise((resolve) => {
64
+ const req = http.get(`${API_URL}/status`, (res) => {
65
+ resolve(res.statusCode === 200);
66
+ });
67
+ req.on('error', () => resolve(false));
68
+ req.setTimeout(2000, () => {
69
+ req.destroy();
70
+ resolve(false);
71
+ });
72
+ });
73
+ }
74
+ function savePid(pid) {
75
+ const dir = path.dirname(PID_FILE);
76
+ if (!fs.existsSync(dir)) {
77
+ fs.mkdirSync(dir, { recursive: true });
78
+ }
79
+ fs.writeFileSync(PID_FILE, String(pid));
80
+ }
81
+ function readPid() {
82
+ if (fs.existsSync(PID_FILE)) {
83
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
84
+ return isNaN(pid) ? null : pid;
85
+ }
86
+ return null;
87
+ }
88
+ function deletePid() {
89
+ if (fs.existsSync(PID_FILE)) {
90
+ fs.unlinkSync(PID_FILE);
91
+ }
92
+ }
93
+ function checkHooksInstalled() {
94
+ const settingsFile = path.join(process.env.HOME || '~', '.claude', 'settings.json');
95
+ if (!fs.existsSync(settingsFile))
96
+ return false;
97
+ try {
98
+ const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
99
+ return !!(settings.hooks?.SessionStart?.some((h) => h.hooks?.some((hook) => hook.command?.includes('session-start.js'))) &&
100
+ settings.hooks?.Stop?.some((h) => h.hooks?.some((hook) => hook.command?.includes('stop.js'))) &&
101
+ settings.hooks?.PostToolUse?.some((h) => h.hooks?.some((hook) => hook.command?.includes('post-tool-use.js'))));
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ // ============================================================================
108
+ // Core Commands
109
+ // ============================================================================
110
+ program
111
+ .command('start')
112
+ .description('Start the voice extension daemon')
113
+ .option('-f, --foreground', "Run in foreground (don't daemonize)")
114
+ .action(async (options) => {
115
+ // Load env vars first
116
+ (0, env_1.loadEnvVars)();
117
+ const isRunning = await checkDaemon();
118
+ if (isRunning) {
119
+ console.log('Daemon is already running.');
120
+ return;
121
+ }
122
+ const indexPath = path.join(__dirname, 'index.js');
123
+ if (options.foreground) {
124
+ // Run in foreground
125
+ console.log('Starting daemon in foreground...');
126
+ require('./index');
127
+ }
128
+ else {
129
+ // Run as daemon
130
+ console.log('Starting daemon...');
131
+ // Ensure log directory exists
132
+ const logDir = path.dirname(LOG_FILE);
133
+ if (!fs.existsSync(logDir)) {
134
+ fs.mkdirSync(logDir, { recursive: true });
135
+ }
136
+ const logStream = fs.openSync(LOG_FILE, 'a');
137
+ const child = (0, child_process_1.spawn)('node', [indexPath], {
138
+ detached: true,
139
+ stdio: ['ignore', logStream, logStream],
140
+ env: process.env,
141
+ });
142
+ if (child.pid) {
143
+ savePid(child.pid);
144
+ child.unref();
145
+ // Wait a moment and check if it started
146
+ await new Promise((r) => setTimeout(r, 1500));
147
+ if (await checkDaemon()) {
148
+ console.log('Daemon started successfully.');
149
+ console.log(`PID: ${child.pid}`);
150
+ console.log(`Logs: ${LOG_FILE}`);
151
+ }
152
+ else {
153
+ console.error('Daemon failed to start. Check logs:');
154
+ console.error(` tail -f ${LOG_FILE}`);
155
+ deletePid();
156
+ process.exit(1);
157
+ }
158
+ }
159
+ }
160
+ });
161
+ program
162
+ .command('stop')
163
+ .description('Stop the voice extension daemon')
164
+ .action(async () => {
165
+ const pid = readPid();
166
+ if (pid) {
167
+ try {
168
+ process.kill(pid, 'SIGTERM');
169
+ console.log('Daemon stopped.');
170
+ deletePid();
171
+ }
172
+ catch {
173
+ console.log('Daemon was not running.');
174
+ deletePid();
175
+ }
176
+ }
177
+ else {
178
+ console.log('No daemon PID found.');
179
+ }
180
+ });
181
+ program
182
+ .command('restart')
183
+ .description('Restart the voice extension daemon')
184
+ .action(async () => {
185
+ const pid = readPid();
186
+ if (pid) {
187
+ try {
188
+ process.kill(pid, 'SIGTERM');
189
+ console.log('Stopping daemon...');
190
+ deletePid();
191
+ await new Promise((r) => setTimeout(r, 1000));
192
+ }
193
+ catch {
194
+ // Already stopped
195
+ }
196
+ }
197
+ // Start again
198
+ console.log('Starting daemon...');
199
+ (0, env_1.loadEnvVars)();
200
+ const indexPath = path.join(__dirname, 'index.js');
201
+ const logDir = path.dirname(LOG_FILE);
202
+ if (!fs.existsSync(logDir)) {
203
+ fs.mkdirSync(logDir, { recursive: true });
204
+ }
205
+ const logStream = fs.openSync(LOG_FILE, 'a');
206
+ const child = (0, child_process_1.spawn)('node', [indexPath], {
207
+ detached: true,
208
+ stdio: ['ignore', logStream, logStream],
209
+ env: process.env,
210
+ });
211
+ if (child.pid) {
212
+ savePid(child.pid);
213
+ child.unref();
214
+ await new Promise((r) => setTimeout(r, 1500));
215
+ if (await checkDaemon()) {
216
+ console.log('Daemon restarted successfully.');
217
+ console.log(`PID: ${child.pid}`);
218
+ }
219
+ else {
220
+ console.error('Daemon failed to start. Check logs.');
221
+ deletePid();
222
+ process.exit(1);
223
+ }
224
+ }
225
+ });
226
+ program
227
+ .command('status')
228
+ .description('Check daemon status and show configuration')
229
+ .action(async () => {
230
+ const isRunning = await checkDaemon();
231
+ const config = (0, config_1.loadConfig)();
232
+ console.log('\n Claude Voice Extension Status\n');
233
+ console.log(` Daemon: ${isRunning ? '\x1b[32mRunning\x1b[0m' : '\x1b[31mStopped\x1b[0m'}`);
234
+ if (isRunning) {
235
+ // Get detailed status from daemon
236
+ const statusPromise = new Promise((resolve) => {
237
+ const req = http.get(`${API_URL}/status`, (res) => {
238
+ let data = '';
239
+ res.on('data', (chunk) => (data += chunk));
240
+ res.on('end', () => {
241
+ try {
242
+ const status = JSON.parse(data);
243
+ console.log(` TTS: ${status.tts.provider} (${status.tts.ready ? 'ready' : 'not ready'})`);
244
+ console.log(` STT: ${status.stt.provider} (${status.stt.ready ? 'ready' : 'not ready'})`);
245
+ console.log(` Wake Word: ${status.wakeWord.enabled ? 'enabled' : 'disabled'}`);
246
+ }
247
+ catch {
248
+ // Ignore parse errors
249
+ }
250
+ resolve();
251
+ });
252
+ });
253
+ req.on('error', () => resolve());
254
+ req.setTimeout(2000, () => {
255
+ req.destroy();
256
+ resolve();
257
+ });
258
+ });
259
+ await statusPromise;
260
+ }
261
+ else {
262
+ console.log(` TTS: ${config.tts.provider}`);
263
+ console.log(` STT: ${config.stt.provider}`);
264
+ console.log(` Wake Word: ${config.wakeWord.enabled ? 'enabled' : 'disabled'}`);
265
+ }
266
+ console.log(` Hooks: ${checkHooksInstalled() ? 'installed' : 'not installed'}`);
267
+ console.log(` Config: ${(0, config_1.getConfigPath)()}`);
268
+ console.log('');
269
+ });
270
+ // ============================================================================
271
+ // Setup Command
272
+ // ============================================================================
273
+ program
274
+ .command('setup')
275
+ .description('Interactive first-run setup wizard')
276
+ .action(async () => {
277
+ // Dynamically import to avoid issues if not installed
278
+ const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer')));
279
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
280
+ const ora = (await Promise.resolve().then(() => __importStar(require('ora')))).default;
281
+ console.log(chalk.bold.blue('\n Welcome to Claude Voice Extension Setup!\n'));
282
+ console.log(' This wizard will help you configure voice features for Claude Code.\n');
283
+ const config = (0, config_1.loadConfig)();
284
+ const caps = (0, platform_1.getPlatformCapabilities)();
285
+ // Step 1: Platform detection
286
+ console.log(chalk.bold(' Step 1: Platform Detection\n'));
287
+ console.log(` Platform: ${caps.platform}`);
288
+ console.log(` Native TTS: ${caps.nativeTTS ? caps.nativeTTSCommand : 'not available'}`);
289
+ console.log(` Terminal Injection: ${caps.terminalInjection}\n`);
290
+ const instructions = (0, platform_1.getInstallInstructions)();
291
+ if (instructions.length > 0) {
292
+ console.log(chalk.yellow(' Missing dependencies:'));
293
+ instructions.forEach((i) => console.log(` - ${i}`));
294
+ console.log('');
295
+ }
296
+ // Step 2: TTS Configuration
297
+ console.log(chalk.bold(' Step 2: Text-to-Speech Configuration\n'));
298
+ const ttsChoices = [];
299
+ if (caps.nativeTTS) {
300
+ ttsChoices.push({
301
+ name: `${caps.nativeTTSCommand} (built-in, no API key)`,
302
+ value: caps.platform === 'darwin' ? 'macos-say' : 'espeak',
303
+ });
304
+ }
305
+ ttsChoices.push({ name: 'Piper TTS (free, local, high quality neural voices)', value: 'piper' }, { name: 'OpenAI TTS (high quality, requires API key)', value: 'openai' }, { name: 'ElevenLabs (premium voices, requires API key)', value: 'elevenlabs' }, { name: 'Disabled', value: 'disabled' });
306
+ const ttsAnswers = await inquirer.default.prompt([
307
+ {
308
+ type: 'list',
309
+ name: 'provider',
310
+ message: 'Which TTS provider would you like to use?',
311
+ choices: ttsChoices,
312
+ default: config.tts.provider,
313
+ },
314
+ {
315
+ type: 'confirm',
316
+ name: 'autoSpeak',
317
+ message: "Automatically speak Claude's responses?",
318
+ default: config.tts.autoSpeak,
319
+ },
320
+ ]);
321
+ config.tts.provider = ttsAnswers.provider;
322
+ config.tts.autoSpeak = ttsAnswers.autoSpeak;
323
+ // Step 3: STT Configuration
324
+ console.log(chalk.bold('\n Step 3: Speech-to-Text Configuration\n'));
325
+ const sttAnswers = await inquirer.default.prompt([
326
+ {
327
+ type: 'list',
328
+ name: 'provider',
329
+ message: 'Which STT provider would you like to use?',
330
+ choices: [
331
+ { name: 'Sherpa-ONNX (FREE, embedded, offline)', value: 'sherpa-onnx' },
332
+ { name: 'OpenAI Whisper API (fast, requires API key)', value: 'openai' },
333
+ { name: 'Local Whisper (free, requires Python)', value: 'whisper-local' },
334
+ { name: 'Disabled', value: 'disabled' },
335
+ ],
336
+ default: config.stt.provider,
337
+ },
338
+ {
339
+ type: 'input',
340
+ name: 'language',
341
+ message: 'Default language code (e.g., en, tr, de):',
342
+ default: config.stt.language,
343
+ },
344
+ ]);
345
+ config.stt.provider = sttAnswers.provider;
346
+ config.stt.language = sttAnswers.language;
347
+ // Step 4: Wake Word
348
+ console.log(chalk.bold('\n Step 4: Wake Word Configuration\n'));
349
+ const wakeWordAnswers = await inquirer.default.prompt([
350
+ {
351
+ type: 'confirm',
352
+ name: 'enabled',
353
+ message: 'Enable wake word detection (say "Jarvis" to start speaking)?',
354
+ default: config.wakeWord.enabled,
355
+ },
356
+ {
357
+ type: 'confirm',
358
+ name: 'playSound',
359
+ message: 'Play sound when wake word is detected?',
360
+ default: config.wakeWord.playSound,
361
+ when: (answers) => answers.enabled,
362
+ },
363
+ ]);
364
+ config.wakeWord.enabled = wakeWordAnswers.enabled;
365
+ if (wakeWordAnswers.playSound !== undefined) {
366
+ config.wakeWord.playSound = wakeWordAnswers.playSound;
367
+ }
368
+ // Step 5: Voice Notifications
369
+ console.log(chalk.bold('\n Step 5: Voice Notifications\n'));
370
+ const notifAnswers = await inquirer.default.prompt([
371
+ {
372
+ type: 'confirm',
373
+ name: 'enabled',
374
+ message: 'Enable voice notifications (permission prompts, etc.)?',
375
+ default: config.notifications.enabled,
376
+ },
377
+ ]);
378
+ config.notifications.enabled = notifAnswers.enabled;
379
+ // Step 6: Voice Output Formatting
380
+ console.log(chalk.bold('\n Step 6: Voice Output Formatting\n'));
381
+ console.log(' This feature makes Claude structure responses with TTS-friendly summaries.');
382
+ console.log(' Claude will add a spoken abstract at the start of each response.\n');
383
+ const voiceOutputAnswers = await inquirer.default.prompt([
384
+ {
385
+ type: 'confirm',
386
+ name: 'enabled',
387
+ message: 'Enable voice-friendly output formatting?',
388
+ default: config.voiceOutput?.enabled !== false,
389
+ },
390
+ ]);
391
+ if (!config.voiceOutput) {
392
+ config.voiceOutput = {
393
+ enabled: true,
394
+ abstractMarker: '<!-- TTS -->',
395
+ maxAbstractLength: 200,
396
+ promptTemplate: null,
397
+ };
398
+ }
399
+ config.voiceOutput.enabled = voiceOutputAnswers.enabled;
400
+ // Step 7: Install hooks
401
+ console.log(chalk.bold('\n Step 7: Claude Code Integration\n'));
402
+ const hooksInstalled = checkHooksInstalled();
403
+ if (!hooksInstalled) {
404
+ const hookAnswers = await inquirer.default.prompt([
405
+ {
406
+ type: 'confirm',
407
+ name: 'install',
408
+ message: 'Install Claude Code hooks now?',
409
+ default: true,
410
+ },
411
+ ]);
412
+ if (hookAnswers.install) {
413
+ const spinner = ora('Installing hooks...').start();
414
+ try {
415
+ installHooksHelper();
416
+ spinner.succeed('Hooks installed successfully');
417
+ }
418
+ catch (error) {
419
+ spinner.fail('Failed to install hooks');
420
+ console.error(error);
421
+ }
422
+ }
423
+ }
424
+ else {
425
+ console.log(' Hooks are already installed.\n');
426
+ }
427
+ // Save configuration
428
+ const spinner = ora('Saving configuration...').start();
429
+ (0, config_1.saveConfig)(config);
430
+ spinner.succeed('Configuration saved');
431
+ // Summary
432
+ console.log(chalk.bold.green('\n Setup Complete!\n'));
433
+ console.log(' Your configuration:');
434
+ console.log(` TTS Provider: ${config.tts.provider}`);
435
+ console.log(` Auto-Speak: ${config.tts.autoSpeak ? 'enabled' : 'disabled'}`);
436
+ console.log(` STT Provider: ${config.stt.provider}`);
437
+ console.log(` Language: ${config.stt.language}`);
438
+ console.log(` Wake Word: ${config.wakeWord.enabled ? 'enabled' : 'disabled'}`);
439
+ console.log(` Notifications: ${config.notifications.enabled ? 'enabled' : 'disabled'}`);
440
+ console.log(` Voice Output: ${config.voiceOutput?.enabled ? 'enabled' : 'disabled'}`);
441
+ console.log(chalk.bold('\n Next Steps:\n'));
442
+ console.log(' 1. Start the daemon: claude-voice start');
443
+ console.log(' 2. Test TTS: claude-voice test-tts "Hello world"');
444
+ console.log(' 3. Check status: claude-voice status\n');
445
+ });
446
+ // ============================================================================
447
+ // Doctor Command
448
+ // ============================================================================
449
+ program
450
+ .command('doctor')
451
+ .description('Diagnose issues and check dependencies')
452
+ .action(async () => {
453
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
454
+ const ora = (await Promise.resolve().then(() => __importStar(require('ora')))).default;
455
+ console.log(chalk.bold('\n Claude Voice Extension - System Check\n'));
456
+ // Check Node.js version
457
+ let spinner = ora('Checking Node.js version...').start();
458
+ const nodeVersion = process.version;
459
+ const major = parseInt(nodeVersion.slice(1).split('.')[0], 10);
460
+ if (major >= 18) {
461
+ spinner.succeed(`Node.js: ${nodeVersion}`);
462
+ }
463
+ else {
464
+ spinner.fail(`Node.js: ${nodeVersion} (requires >= 18.0.0)`);
465
+ }
466
+ // Check platform
467
+ spinner = ora('Checking platform...').start();
468
+ const caps = (0, platform_1.getPlatformCapabilities)();
469
+ if (caps.platform !== 'unsupported') {
470
+ spinner.succeed(`Platform: ${caps.platform}`);
471
+ }
472
+ else {
473
+ spinner.warn(`Platform: ${process.platform} (not fully supported)`);
474
+ }
475
+ // Check native TTS
476
+ spinner = ora('Checking native TTS...').start();
477
+ if (caps.nativeTTS) {
478
+ spinner.succeed(`Native TTS: ${caps.nativeTTSCommand}`);
479
+ }
480
+ else {
481
+ spinner.warn('Native TTS: not available');
482
+ }
483
+ // Check terminal injection
484
+ spinner = ora('Checking terminal injection...').start();
485
+ if (caps.terminalInjection !== 'none') {
486
+ spinner.succeed(`Terminal injection: ${caps.terminalInjection}`);
487
+ }
488
+ else {
489
+ spinner.warn('Terminal injection: not available');
490
+ }
491
+ // Check config
492
+ spinner = ora('Checking configuration...').start();
493
+ try {
494
+ const config = (0, config_1.loadConfig)();
495
+ spinner.succeed(`Configuration: ${(0, config_1.getConfigPath)()}`);
496
+ }
497
+ catch (error) {
498
+ spinner.fail('Configuration: invalid or missing');
499
+ }
500
+ // Check hooks
501
+ spinner = ora('Checking hooks...').start();
502
+ if (checkHooksInstalled()) {
503
+ spinner.succeed('Hooks: installed');
504
+ }
505
+ else {
506
+ spinner.warn('Hooks: not installed (run: claude-voice hooks install)');
507
+ }
508
+ // Check API keys
509
+ spinner = ora('Checking API keys...').start();
510
+ (0, env_1.loadEnvVars)();
511
+ const apiKeys = (0, env_1.checkApiKeys)();
512
+ spinner.succeed('API Keys:');
513
+ for (const { key, configured, source } of apiKeys) {
514
+ const status = configured ? chalk.green('configured') : chalk.yellow('not configured');
515
+ console.log(` ${key}: ${status} (${source})`);
516
+ }
517
+ // Check daemon
518
+ spinner = ora('Checking daemon...').start();
519
+ const isRunning = await checkDaemon();
520
+ if (isRunning) {
521
+ spinner.succeed('Daemon: running');
522
+ }
523
+ else {
524
+ spinner.info('Daemon: not running');
525
+ }
526
+ console.log('');
527
+ });
528
+ // ============================================================================
529
+ // Hooks Commands
530
+ // ============================================================================
531
+ const hooksCommand = program.command('hooks').description('Manage Claude Code hooks');
532
+ function installHooksHelper() {
533
+ const claudeSettingsDir = path.join(process.env.HOME || '~', '.claude');
534
+ const settingsFile = path.join(claudeSettingsDir, 'settings.json');
535
+ // Ensure directory exists
536
+ if (!fs.existsSync(claudeSettingsDir)) {
537
+ fs.mkdirSync(claudeSettingsDir, { recursive: true });
538
+ }
539
+ // Load existing settings or create new
540
+ let settings = {};
541
+ if (fs.existsSync(settingsFile)) {
542
+ try {
543
+ settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
544
+ }
545
+ catch {
546
+ // Start fresh
547
+ }
548
+ }
549
+ const hooksDir = path.join(__dirname, '..', 'hooks');
550
+ // Define hooks
551
+ const hooks = {
552
+ SessionStart: [
553
+ {
554
+ hooks: [
555
+ {
556
+ type: 'command',
557
+ command: `node "${path.join(hooksDir, 'session-start.js')}"`,
558
+ timeout: 10,
559
+ },
560
+ ],
561
+ },
562
+ ],
563
+ Stop: [
564
+ {
565
+ hooks: [
566
+ {
567
+ type: 'command',
568
+ command: `node "${path.join(hooksDir, 'stop.js')}"`,
569
+ timeout: 10,
570
+ },
571
+ ],
572
+ },
573
+ ],
574
+ Notification: [
575
+ {
576
+ matcher: 'permission_prompt|idle_prompt',
577
+ hooks: [
578
+ {
579
+ type: 'command',
580
+ command: `node "${path.join(hooksDir, 'notification.js')}"`,
581
+ timeout: 5,
582
+ },
583
+ ],
584
+ },
585
+ ],
586
+ PostToolUse: [
587
+ {
588
+ hooks: [
589
+ {
590
+ type: 'command',
591
+ command: `node "${path.join(hooksDir, 'post-tool-use.js')}"`,
592
+ timeout: 5,
593
+ },
594
+ ],
595
+ },
596
+ ],
597
+ };
598
+ // Merge hooks
599
+ settings.hooks = {
600
+ ...(settings.hooks || {}),
601
+ ...hooks,
602
+ };
603
+ // Save settings
604
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
605
+ }
606
+ hooksCommand
607
+ .command('install')
608
+ .description('Install Claude Code hooks')
609
+ .action(() => {
610
+ installHooksHelper();
611
+ console.log('Hooks installed successfully!');
612
+ console.log(`Settings file: ${path.join(process.env.HOME || '~', '.claude', 'settings.json')}`);
613
+ });
614
+ hooksCommand
615
+ .command('uninstall')
616
+ .description('Remove Claude Code hooks')
617
+ .action(() => {
618
+ const settingsFile = path.join(process.env.HOME || '~', '.claude', 'settings.json');
619
+ if (!fs.existsSync(settingsFile)) {
620
+ console.log('No settings file found.');
621
+ return;
622
+ }
623
+ try {
624
+ const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
625
+ if (settings.hooks) {
626
+ delete settings.hooks.SessionStart;
627
+ delete settings.hooks.Stop;
628
+ delete settings.hooks.Notification;
629
+ delete settings.hooks.PostToolUse;
630
+ // Remove hooks object if empty
631
+ if (Object.keys(settings.hooks).length === 0) {
632
+ delete settings.hooks;
633
+ }
634
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
635
+ console.log('Hooks uninstalled successfully!');
636
+ }
637
+ else {
638
+ console.log('No hooks found to uninstall.');
639
+ }
640
+ }
641
+ catch (error) {
642
+ console.error('Failed to uninstall hooks:', error);
643
+ }
644
+ });
645
+ hooksCommand
646
+ .command('status')
647
+ .description('Check hooks installation status')
648
+ .action(() => {
649
+ const settingsFile = path.join(process.env.HOME || '~', '.claude', 'settings.json');
650
+ if (!fs.existsSync(settingsFile)) {
651
+ console.log('Status: Not installed (no settings file)');
652
+ return;
653
+ }
654
+ if (checkHooksInstalled()) {
655
+ console.log('Status: Installed');
656
+ console.log(`Settings: ${settingsFile}`);
657
+ }
658
+ else {
659
+ console.log('Status: Not installed');
660
+ }
661
+ });
662
+ // Keep old commands for backward compatibility
663
+ program
664
+ .command('install-hooks')
665
+ .description('Install Claude Code hooks (alias for: hooks install)')
666
+ .action(() => {
667
+ installHooksHelper();
668
+ console.log('Hooks installed successfully!');
669
+ });
670
+ program
671
+ .command('uninstall-hooks')
672
+ .description('Remove Claude Code hooks (alias for: hooks uninstall)')
673
+ .action(() => {
674
+ const settingsFile = path.join(process.env.HOME || '~', '.claude', 'settings.json');
675
+ if (!fs.existsSync(settingsFile)) {
676
+ console.log('No settings file found.');
677
+ return;
678
+ }
679
+ try {
680
+ const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
681
+ if (settings.hooks) {
682
+ delete settings.hooks.SessionStart;
683
+ delete settings.hooks.Stop;
684
+ delete settings.hooks.Notification;
685
+ if (Object.keys(settings.hooks).length === 0) {
686
+ delete settings.hooks;
687
+ }
688
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2));
689
+ console.log('Hooks uninstalled successfully!');
690
+ }
691
+ else {
692
+ console.log('No hooks found to uninstall.');
693
+ }
694
+ }
695
+ catch (error) {
696
+ console.error('Failed to uninstall hooks:', error);
697
+ }
698
+ });
699
+ // ============================================================================
700
+ // Config Commands
701
+ // ============================================================================
702
+ program
703
+ .command('config [action] [args...]')
704
+ .description('View or modify configuration')
705
+ .option('-p, --path', 'Show configuration file path')
706
+ .action((action, args, options) => {
707
+ if (options.path) {
708
+ console.log((0, config_1.getConfigPath)());
709
+ return;
710
+ }
711
+ if (!action) {
712
+ // Default: show full config
713
+ const config = (0, config_1.loadConfig)();
714
+ console.log(JSON.stringify(config, null, 2));
715
+ return;
716
+ }
717
+ switch (action) {
718
+ case 'get':
719
+ if (args.length === 0) {
720
+ console.error('Usage: claude-voice config get <key>');
721
+ process.exit(1);
722
+ }
723
+ const getValue = (0, config_1.getConfigValue)(args[0]);
724
+ console.log(JSON.stringify(getValue, null, 2));
725
+ break;
726
+ case 'set':
727
+ if (args.length === 0) {
728
+ console.error('Usage: claude-voice config set <key>=<value>');
729
+ process.exit(1);
730
+ }
731
+ const [key, ...valueParts] = args[0].split('=');
732
+ const value = valueParts.join('=');
733
+ (0, config_1.setConfigValue)(key, value);
734
+ console.log(`Set ${key} = ${value}`);
735
+ break;
736
+ case 'reset':
737
+ (0, config_1.resetConfig)();
738
+ console.log('Configuration reset to defaults.');
739
+ break;
740
+ case 'edit':
741
+ const editor = process.env.EDITOR || 'nano';
742
+ const configPath = (0, config_1.getConfigPath)();
743
+ // Ensure config file exists
744
+ if (!fs.existsSync(configPath)) {
745
+ (0, config_1.saveConfig)((0, config_1.loadConfig)());
746
+ }
747
+ (0, child_process_1.execSync)(`${editor} "${configPath}"`, { stdio: 'inherit' });
748
+ break;
749
+ default:
750
+ console.error(`Unknown config action: ${action}`);
751
+ console.error('Available actions: get, set, reset, edit');
752
+ process.exit(1);
753
+ }
754
+ });
755
+ // ============================================================================
756
+ // Voice Output Commands
757
+ // ============================================================================
758
+ const outputCommand = program.command('output').description('Manage voice-friendly output formatting');
759
+ outputCommand
760
+ .command('enable')
761
+ .description('Enable voice-friendly output formatting')
762
+ .action(() => {
763
+ (0, config_1.setConfigValue)('voiceOutput.enabled', true);
764
+ console.log('Voice output formatting enabled.');
765
+ console.log('Claude will now structure responses with TTS-friendly abstracts.');
766
+ console.log('Note: Restart your Claude Code session for changes to take effect.');
767
+ });
768
+ outputCommand
769
+ .command('disable')
770
+ .description('Disable voice-friendly output formatting')
771
+ .action(() => {
772
+ (0, config_1.setConfigValue)('voiceOutput.enabled', false);
773
+ console.log('Voice output formatting disabled.');
774
+ console.log('Claude will use normal response formatting.');
775
+ console.log('Note: Restart your Claude Code session for changes to take effect.');
776
+ });
777
+ outputCommand
778
+ .command('status')
779
+ .description('Show voice output formatting status')
780
+ .action(() => {
781
+ const config = (0, config_1.loadConfig)();
782
+ const enabled = config.voiceOutput?.enabled !== false;
783
+ console.log(`\nVoice Output Formatting: ${enabled ? '\x1b[32menabled\x1b[0m' : '\x1b[31mdisabled\x1b[0m'}`);
784
+ console.log(`Abstract Marker: ${config.voiceOutput?.abstractMarker || '<!-- TTS -->'}`);
785
+ console.log(`Max Abstract Length: ${config.voiceOutput?.maxAbstractLength || 200} characters`);
786
+ console.log(`Custom Template: ${config.voiceOutput?.promptTemplate ? 'yes' : 'using default'}`);
787
+ console.log('');
788
+ });
789
+ outputCommand
790
+ .command('config')
791
+ .description('Configure voice output settings')
792
+ .option('-m, --marker <marker>', 'Set abstract marker (default: <!-- TTS -->)')
793
+ .option('-l, --length <length>', 'Set max abstract length in characters')
794
+ .action((options) => {
795
+ if (options.marker) {
796
+ (0, config_1.setConfigValue)('voiceOutput.abstractMarker', options.marker);
797
+ console.log(`Abstract marker set to: ${options.marker}`);
798
+ }
799
+ if (options.length) {
800
+ const length = parseInt(options.length, 10);
801
+ if (isNaN(length) || length < 50) {
802
+ console.error('Length must be a number >= 50');
803
+ process.exit(1);
804
+ }
805
+ (0, config_1.setConfigValue)('voiceOutput.maxAbstractLength', length);
806
+ console.log(`Max abstract length set to: ${length} characters`);
807
+ }
808
+ if (!options.marker && !options.length) {
809
+ console.log('Usage: claude-voice output config --marker "<!-- TTS -->" --length 200');
810
+ }
811
+ });
812
+ // ============================================================================
813
+ // Test Commands
814
+ // ============================================================================
815
+ program
816
+ .command('test-tts')
817
+ .description('Test text-to-speech')
818
+ .argument('[text]', 'Text to speak', 'Hello! I am Claude Voice Extension.')
819
+ .action(async (text) => {
820
+ (0, env_1.loadEnvVars)();
821
+ const config = (0, config_1.loadConfig)();
822
+ const ttsManager = new tts_1.TTSManager(config.tts);
823
+ console.log(`Testing TTS with provider: ${config.tts.provider}`);
824
+ console.log(`Speaking: "${text}"`);
825
+ try {
826
+ await ttsManager.speak(text);
827
+ console.log('TTS test completed.');
828
+ }
829
+ catch (error) {
830
+ console.error('TTS test failed:', error);
831
+ }
832
+ });
833
+ program
834
+ .command('test-stt')
835
+ .description('Test speech-to-text')
836
+ .argument('<audio-file>', 'Path to audio file')
837
+ .action(async (audioFile) => {
838
+ (0, env_1.loadEnvVars)();
839
+ const config = (0, config_1.loadConfig)();
840
+ const sttManager = new stt_1.STTManager(config.stt);
841
+ console.log(`Testing STT with provider: ${config.stt.provider}`);
842
+ console.log(`Audio file: ${audioFile}`);
843
+ try {
844
+ const transcript = await sttManager.transcribe(audioFile);
845
+ console.log(`Transcript: "${transcript}"`);
846
+ }
847
+ catch (error) {
848
+ console.error('STT test failed:', error);
849
+ }
850
+ });
851
+ // ============================================================================
852
+ // Utility Commands
853
+ // ============================================================================
854
+ program
855
+ .command('voices')
856
+ .description('List available TTS voices')
857
+ .option('-p, --provider <provider>', 'Filter by provider')
858
+ .action(async (options) => {
859
+ const caps = (0, platform_1.getPlatformCapabilities)();
860
+ if (!options.provider || options.provider === 'macos-say') {
861
+ if (caps.platform === 'darwin') {
862
+ console.log('\nmacOS Say Voices:');
863
+ try {
864
+ const output = (0, child_process_1.execSync)('say -v "?"', { encoding: 'utf-8' });
865
+ const voices = output
866
+ .split('\n')
867
+ .filter((line) => line.trim())
868
+ .slice(0, 20); // Show first 20
869
+ voices.forEach((v) => console.log(` ${v}`));
870
+ if (output.split('\n').length > 20) {
871
+ console.log(' ... (run "say -v ?" for full list)');
872
+ }
873
+ }
874
+ catch {
875
+ console.log(' Unable to list voices');
876
+ }
877
+ }
878
+ }
879
+ if (!options.provider || options.provider === 'openai') {
880
+ console.log('\nOpenAI TTS Voices:');
881
+ console.log(' alloy, echo, fable, onyx, nova, shimmer');
882
+ }
883
+ if (!options.provider || options.provider === 'elevenlabs') {
884
+ console.log('\nElevenLabs Voices:');
885
+ console.log(' Configure voice ID in config: tts.elevenlabs.voiceId');
886
+ console.log(' Browse voices at: https://elevenlabs.io/voice-library');
887
+ }
888
+ console.log('');
889
+ });
890
+ program
891
+ .command('devices')
892
+ .description('List available audio input devices')
893
+ .action(async () => {
894
+ console.log('\nAudio Input Devices:\n');
895
+ try {
896
+ // Try to use PvRecorder to list devices
897
+ const pvRecorderModule = await Promise.resolve().then(() => __importStar(require('@picovoice/pvrecorder-node')));
898
+ const devices = pvRecorderModule.PvRecorder.getAvailableDevices();
899
+ devices.forEach((device, index) => {
900
+ console.log(` [${index}] ${device}`);
901
+ });
902
+ }
903
+ catch {
904
+ console.log(' Unable to list audio devices.');
905
+ console.log(' Install @picovoice/pvrecorder-node for device listing.');
906
+ }
907
+ console.log('');
908
+ });
909
+ program
910
+ .command('logs')
911
+ .description('View daemon logs')
912
+ .option('-f, --follow', 'Follow log output')
913
+ .option('-n, --lines <n>', 'Show last n lines', '50')
914
+ .action((options) => {
915
+ if (!fs.existsSync(LOG_FILE)) {
916
+ console.log('No log file found.');
917
+ return;
918
+ }
919
+ if (options.follow) {
920
+ // Use tail -f
921
+ const tail = (0, child_process_1.spawn)('tail', ['-f', LOG_FILE], { stdio: 'inherit' });
922
+ tail.on('error', () => {
923
+ console.error('Unable to follow logs. Is tail available?');
924
+ });
925
+ }
926
+ else {
927
+ // Show last n lines
928
+ try {
929
+ const output = (0, child_process_1.execSync)(`tail -n ${options.lines} "${LOG_FILE}"`, { encoding: 'utf-8' });
930
+ console.log(output);
931
+ }
932
+ catch {
933
+ // Fallback: read entire file
934
+ const content = fs.readFileSync(LOG_FILE, 'utf-8');
935
+ const lines = content.split('\n').slice(-parseInt(options.lines, 10));
936
+ console.log(lines.join('\n'));
937
+ }
938
+ }
939
+ });
940
+ // ============================================================================
941
+ // Model Commands
942
+ // ============================================================================
943
+ const modelCommand = program.command('model').description('Manage STT models');
944
+ modelCommand
945
+ .command('list')
946
+ .description('List available and installed STT models')
947
+ .action(async () => {
948
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
949
+ console.log(chalk.bold('\n Available STT Models (Sherpa-ONNX)\n'));
950
+ const models = (0, sherpa_onnx_1.listModels)();
951
+ for (const model of models) {
952
+ const status = model.installed ? chalk.green('[installed]') : chalk.gray('[not installed]');
953
+ console.log(` ${status} ${model.id}`);
954
+ console.log(` ${model.name}`);
955
+ console.log(` Languages: ${model.languages.slice(0, 5).join(', ')}...`);
956
+ console.log('');
957
+ }
958
+ console.log(' To download a model: claude-voice model download <model-id>');
959
+ console.log(' To use a model: claude-voice config set stt.provider=sherpa-onnx');
960
+ console.log('');
961
+ });
962
+ modelCommand
963
+ .command('download <model-id>')
964
+ .description('Download an STT model')
965
+ .action(async (modelId) => {
966
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
967
+ const ora = (await Promise.resolve().then(() => __importStar(require('ora')))).default;
968
+ if (!sherpa_onnx_1.SHERPA_MODELS[modelId]) {
969
+ console.error(chalk.red(`Unknown model: ${modelId}`));
970
+ console.log('\nAvailable models:');
971
+ Object.keys(sherpa_onnx_1.SHERPA_MODELS).forEach((id) => console.log(` - ${id}`));
972
+ process.exit(1);
973
+ }
974
+ console.log(chalk.bold(`\n Downloading ${modelId}...\n`));
975
+ try {
976
+ await (0, sherpa_onnx_1.downloadModel)(modelId);
977
+ console.log(chalk.green('\n Model downloaded successfully!'));
978
+ console.log('\n To use this model:');
979
+ console.log(' claude-voice config set stt.provider=sherpa-onnx');
980
+ console.log(` claude-voice config set stt.sherpaOnnx.model=${modelId}`);
981
+ console.log('');
982
+ }
983
+ catch (error) {
984
+ console.error(chalk.red('\n Download failed:'), error);
985
+ process.exit(1);
986
+ }
987
+ });
988
+ modelCommand
989
+ .command('remove <model-id>')
990
+ .description('Remove an installed STT model')
991
+ .action(async (modelId) => {
992
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
993
+ const modelInfo = sherpa_onnx_1.SHERPA_MODELS[modelId];
994
+ if (!modelInfo) {
995
+ console.error(chalk.red(`Unknown model: ${modelId}`));
996
+ process.exit(1);
997
+ }
998
+ const modelsDir = path.join(process.env.HOME || '~', '.claude-voice', 'models');
999
+ const modelPath = path.join(modelsDir, modelInfo.folder);
1000
+ if (!fs.existsSync(modelPath)) {
1001
+ console.log(`Model not installed: ${modelId}`);
1002
+ return;
1003
+ }
1004
+ try {
1005
+ fs.rmSync(modelPath, { recursive: true, force: true });
1006
+ console.log(chalk.green(`Model removed: ${modelId}`));
1007
+ }
1008
+ catch (error) {
1009
+ console.error(chalk.red('Failed to remove model:'), error);
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+ // ============================================================================
1014
+ // Voice Commands (Piper TTS)
1015
+ // ============================================================================
1016
+ const voiceCommand = program.command('voice').description('Manage Piper TTS voices');
1017
+ voiceCommand
1018
+ .command('list')
1019
+ .description('List available and installed Piper voices')
1020
+ .action(async () => {
1021
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
1022
+ console.log(chalk.bold('\n Available Piper TTS Voices\n'));
1023
+ const voices = (0, piper_1.listVoices)();
1024
+ // Group by language
1025
+ const byLanguage = {};
1026
+ for (const voice of voices) {
1027
+ const lang = voice.language;
1028
+ if (!byLanguage[lang])
1029
+ byLanguage[lang] = [];
1030
+ byLanguage[lang].push(voice);
1031
+ }
1032
+ for (const [lang, langVoices] of Object.entries(byLanguage)) {
1033
+ console.log(chalk.bold(` ${lang}:`));
1034
+ for (const voice of langVoices) {
1035
+ const status = voice.installed ? chalk.green('[installed]') : chalk.gray('[available]');
1036
+ console.log(` ${status} ${voice.id}`);
1037
+ console.log(` ${voice.name}`);
1038
+ }
1039
+ console.log('');
1040
+ }
1041
+ console.log(' To download a voice: claude-voice voice download <voice-id>');
1042
+ console.log(' To use Piper: claude-voice config set tts.provider=piper');
1043
+ console.log('');
1044
+ });
1045
+ voiceCommand
1046
+ .command('download <voice-id>')
1047
+ .description('Download a Piper TTS voice')
1048
+ .action(async (voiceId) => {
1049
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
1050
+ console.log(chalk.bold(`\n Downloading voice: ${voiceId}\n`));
1051
+ try {
1052
+ await (0, piper_1.downloadVoice)(voiceId);
1053
+ console.log(chalk.green('\n Voice downloaded successfully!'));
1054
+ console.log('\n To use this voice:');
1055
+ console.log(' claude-voice config set tts.provider=piper');
1056
+ console.log(` claude-voice config set tts.piper.voice=${voiceId}`);
1057
+ console.log('');
1058
+ }
1059
+ catch (error) {
1060
+ console.error(chalk.red('\n Download failed:'), error);
1061
+ process.exit(1);
1062
+ }
1063
+ });
1064
+ voiceCommand
1065
+ .command('remove <voice-id>')
1066
+ .description('Remove an installed Piper voice')
1067
+ .action(async (voiceId) => {
1068
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
1069
+ try {
1070
+ (0, piper_1.removeVoice)(voiceId);
1071
+ console.log(chalk.green(`Voice removed: ${voiceId}`));
1072
+ }
1073
+ catch (error) {
1074
+ console.error(chalk.red('Failed to remove voice:'), error);
1075
+ process.exit(1);
1076
+ }
1077
+ });
1078
+ voiceCommand
1079
+ .command('status')
1080
+ .description('Check Piper TTS installation status')
1081
+ .action(async () => {
1082
+ const chalk = (await Promise.resolve().then(() => __importStar(require('chalk')))).default;
1083
+ console.log(chalk.bold('\n Piper TTS Status\n'));
1084
+ const piperInstalled = (0, piper_1.isPiperInstalled)();
1085
+ console.log(` Piper Binary: ${piperInstalled ? chalk.green('installed') : chalk.yellow('not installed')}`);
1086
+ const voices = (0, piper_1.listVoices)();
1087
+ const installedVoices = voices.filter((v) => v.installed);
1088
+ console.log(` Installed Voices: ${installedVoices.length}`);
1089
+ if (installedVoices.length > 0) {
1090
+ console.log('');
1091
+ for (const voice of installedVoices) {
1092
+ console.log(` - ${voice.id} (${voice.language})`);
1093
+ }
1094
+ }
1095
+ const config = (0, config_1.loadConfig)();
1096
+ console.log(`\n Current Provider: ${config.tts.provider}`);
1097
+ if (config.tts.provider === 'piper') {
1098
+ console.log(` Current Voice: ${config.tts.piper?.voice || 'not set'}`);
1099
+ }
1100
+ console.log('');
1101
+ });
1102
+ program.parse();
1103
+ //# sourceMappingURL=cli.js.map