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.
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/bin/claude-voice +29 -0
- package/config/default.json +109 -0
- package/config/voice-prompt.md +27 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1103 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +140 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +179 -0
- package/dist/config.js.map +1 -0
- package/dist/env.d.ts +40 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +175 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/index.d.ts +35 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +170 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +185 -0
- package/dist/server.js.map +1 -0
- package/dist/stt/index.d.ts +15 -0
- package/dist/stt/index.d.ts.map +1 -0
- package/dist/stt/index.js +54 -0
- package/dist/stt/index.js.map +1 -0
- package/dist/stt/providers/openai.d.ts +15 -0
- package/dist/stt/providers/openai.d.ts.map +1 -0
- package/dist/stt/providers/openai.js +74 -0
- package/dist/stt/providers/openai.js.map +1 -0
- package/dist/stt/providers/sherpa-onnx.d.ts +50 -0
- package/dist/stt/providers/sherpa-onnx.d.ts.map +1 -0
- package/dist/stt/providers/sherpa-onnx.js +237 -0
- package/dist/stt/providers/sherpa-onnx.js.map +1 -0
- package/dist/stt/providers/whisper-local.d.ts +19 -0
- package/dist/stt/providers/whisper-local.d.ts.map +1 -0
- package/dist/stt/providers/whisper-local.js +141 -0
- package/dist/stt/providers/whisper-local.js.map +1 -0
- package/dist/terminal/input-injector.d.ts +55 -0
- package/dist/terminal/input-injector.d.ts.map +1 -0
- package/dist/terminal/input-injector.js +189 -0
- package/dist/terminal/input-injector.js.map +1 -0
- package/dist/tts/index.d.ts +20 -0
- package/dist/tts/index.d.ts.map +1 -0
- package/dist/tts/index.js +72 -0
- package/dist/tts/index.js.map +1 -0
- package/dist/tts/providers/elevenlabs.d.ts +23 -0
- package/dist/tts/providers/elevenlabs.d.ts.map +1 -0
- package/dist/tts/providers/elevenlabs.js +142 -0
- package/dist/tts/providers/elevenlabs.js.map +1 -0
- package/dist/tts/providers/macos-say.d.ts +17 -0
- package/dist/tts/providers/macos-say.d.ts.map +1 -0
- package/dist/tts/providers/macos-say.js +72 -0
- package/dist/tts/providers/macos-say.js.map +1 -0
- package/dist/tts/providers/openai.d.ts +19 -0
- package/dist/tts/providers/openai.d.ts.map +1 -0
- package/dist/tts/providers/openai.js +118 -0
- package/dist/tts/providers/openai.js.map +1 -0
- package/dist/tts/providers/piper.d.ts +48 -0
- package/dist/tts/providers/piper.d.ts.map +1 -0
- package/dist/tts/providers/piper.js +417 -0
- package/dist/tts/providers/piper.js.map +1 -0
- package/dist/voice-input.d.ts +9 -0
- package/dist/voice-input.d.ts.map +1 -0
- package/dist/voice-input.js +137 -0
- package/dist/voice-input.js.map +1 -0
- package/dist/wake-word/index.d.ts +19 -0
- package/dist/wake-word/index.d.ts.map +1 -0
- package/dist/wake-word/index.js +200 -0
- package/dist/wake-word/index.js.map +1 -0
- package/dist/wake-word/recorder.d.ts +19 -0
- package/dist/wake-word/recorder.d.ts.map +1 -0
- package/dist/wake-word/recorder.js +145 -0
- package/dist/wake-word/recorder.js.map +1 -0
- package/hooks/notification.js +125 -0
- package/hooks/post-tool-use.js +374 -0
- package/hooks/session-start.js +212 -0
- package/hooks/stop.js +254 -0
- package/models/.gitkeep +0 -0
- package/package.json +80 -0
- package/python/stt_service.py +59 -0
- package/python/voice_input.py +154 -0
- package/scripts/install.sh +147 -0
- package/scripts/listen.py +161 -0
- package/scripts/postinstall.js +57 -0
- package/scripts/record.sh +79 -0
- package/scripts/setup-hooks.sh +22 -0
- 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
|