voicci 1.0.9 → 1.1.1

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/README.md CHANGED
@@ -25,11 +25,27 @@ Transform books and PDFs into audiobooks using natural language with Claude Code
25
25
 
26
26
  ### Installation
27
27
 
28
+ **Prerequisites** (one-time):
29
+
30
+ - Node.js 22+
31
+ - Python 3.10+
32
+ - `pip3 install TTS torch torchaudio` — Voicci uses XTTS v2 locally (PyTorch + Coqui TTS; ~2 GB of model weights download on first run)
33
+
34
+ **Install the CLI:**
35
+
28
36
  ```bash
29
37
  npm install -g voicci
30
38
  ```
31
39
 
32
- That's it! The package installs both the CLI tool and Claude Code skill automatically.
40
+ **Verify everything is wired up:**
41
+
42
+ ```bash
43
+ voicci doctor
44
+ ```
45
+
46
+ `voicci doctor` prints a PASS/FAIL table for Node, Python, TTS, PyTorch, acceleration (MPS/CUDA/CPU), and the AI-editor skill directory. If anything fails, it tells you the exact command to fix it.
47
+
48
+ The npm package installs the CLI plus the Claude Code / OpenCode / Cursor / Windsurf skill files. **Restart your AI editor** after install so it picks up `/voicci`.
33
49
 
34
50
  ### Usage with AI Code Editors
35
51
 
package/cli/index.js CHANGED
@@ -21,6 +21,30 @@ const pkg = require('../package.json');
21
21
  const execAsync = promisify(exec);
22
22
  const execFileAsync = promisify(execFile);
23
23
 
24
+ // Cross-platform open command
25
+ function getOpenCommand() {
26
+ const platform = process.platform;
27
+ if (platform === 'darwin') return 'open';
28
+ if (platform === 'win32') return 'start';
29
+ return 'xdg-open'; // Linux
30
+ }
31
+
32
+ // Check if Python TTS dependencies are available
33
+ async function checkTTSDependencies() {
34
+ try {
35
+ await execFileAsync('python3', ['-c', 'from TTS.api import TTS; print("ok")']);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ // Check if input looks like a file path (has a supported extension)
43
+ function looksLikeFilePath(input) {
44
+ const ext = path.extname(input).toLowerCase();
45
+ return ['.pdf', '.txt'].includes(ext);
46
+ }
47
+
24
48
  const program = new Command();
25
49
 
26
50
  program
@@ -78,20 +102,24 @@ program
78
102
 
79
103
  // Process file or search query
80
104
  if (input) {
81
- // Check if input is a file path
82
- if (fs.existsSync(input)) {
83
- // Validate file path for security
84
- try {
85
- const validatedPath = pathValidator.validateFilePath(input, {
86
- mustExist: true,
87
- allowedExtensions: ['.pdf', '.txt']
88
- });
89
- await processFile(validatedPath, options);
90
- } catch (error) {
91
- console.error('Error: Invalid file path');
92
- console.error(error.message);
105
+ if (looksLikeFilePath(input)) {
106
+ // Input has a recognized file extension — must exist
107
+ if (!fs.existsSync(input)) {
108
+ console.error(`Error: File not found: ${input}`);
93
109
  process.exit(1);
94
110
  }
111
+ const validatedPath = pathValidator.validateFilePath(input, {
112
+ mustExist: true,
113
+ allowedExtensions: ['.pdf', '.txt']
114
+ });
115
+ await processFile(validatedPath, options);
116
+ } else if (fs.existsSync(input)) {
117
+ // Existing file without recognized extension
118
+ const validatedPath = pathValidator.validateFilePath(input, {
119
+ mustExist: true,
120
+ allowedExtensions: ['.pdf', '.txt']
121
+ });
122
+ await processFile(validatedPath, options);
95
123
  } else {
96
124
  // Treat as search query
97
125
  console.log(`Searching for: "${input}"\n`);
@@ -154,6 +182,17 @@ async function processFile(filePath, options = {}) {
154
182
  if (summaryOnly) return; // Don't create audiobook job
155
183
  }
156
184
 
185
+ // Check Python TTS dependencies before creating job
186
+ const hasTTS = await checkTTSDependencies();
187
+ if (!hasTTS) {
188
+ console.error('\n❌ Python TTS dependencies not found.');
189
+ console.error('\nTo generate audiobooks, install the required Python packages:');
190
+ console.error(' pip3 install TTS torch torchaudio\n');
191
+ console.error('This is a one-time setup (~2GB download).');
192
+ console.error('After installing, run your command again.\n');
193
+ throw new Error('Python TTS dependencies not installed');
194
+ }
195
+
157
196
  // Create job
158
197
  console.log('📋 Creating job...');
159
198
  const queue = new Queue();
@@ -212,7 +251,7 @@ async function generateSummary(filePath, text, summaryOnly = false) {
212
251
  console.log('─'.repeat(60));
213
252
  console.log(result.summary.substring(0, 500) + '...\n');
214
253
  console.log('─'.repeat(60));
215
- console.log(`\nOpen full summary: open "${summaryPath}"\n`);
254
+ console.log(`\nOpen full summary: ${getOpenCommand()} "${summaryPath}"\n`);
216
255
  }
217
256
  }
218
257
 
@@ -388,7 +427,7 @@ async function openAudiobook(jobId) {
388
427
  if (jobId === true || !jobId) {
389
428
  // Open audiobooks directory
390
429
  const audiobooksDir = config.paths.audiobooks;
391
- await execFileAsync('open', [audiobooksDir]);
430
+ await execFileAsync(getOpenCommand(), [audiobooksDir]);
392
431
  console.log(`Opened: ${audiobooksDir}\n`);
393
432
  } else {
394
433
  // Open specific job
@@ -406,7 +445,7 @@ async function openAudiobook(jobId) {
406
445
  return;
407
446
  }
408
447
 
409
- await execFileAsync('open', [job.output_dir]);
448
+ await execFileAsync(getOpenCommand(), [job.output_dir]);
410
449
  console.log(`Opened: ${job.output_dir}\n`);
411
450
  }
412
451
 
@@ -721,4 +760,133 @@ program
721
760
  console.log();
722
761
  });
723
762
 
763
+ program
764
+ .command('doctor')
765
+ .description('Verify Node, Python, TTS, PyTorch, and AI-editor skill installation')
766
+ .option('--json', 'Output machine-readable JSON')
767
+ .action(async (options) => {
768
+ const results = await runDoctor();
769
+ if (options.json) {
770
+ console.log(JSON.stringify(results, null, 2));
771
+ } else {
772
+ printDoctorReport(results);
773
+ }
774
+ const failed = results.checks.some(c => c.status === 'fail');
775
+ process.exit(failed ? 1 : 0);
776
+ });
777
+
778
+ async function runDoctor() {
779
+ const os = await import('os');
780
+ const checks = [];
781
+
782
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
783
+ checks.push({
784
+ name: 'Node.js ≥ 22',
785
+ status: nodeMajor >= 22 ? 'pass' : 'fail',
786
+ detail: `v${process.versions.node}`,
787
+ fix: nodeMajor >= 22 ? null : 'Install Node 22+: https://nodejs.org/en/download',
788
+ });
789
+
790
+ let pythonCmd = null;
791
+ let pythonVersion = null;
792
+ for (const cand of ['python3', 'python']) {
793
+ try {
794
+ const { stdout } = await execFileAsync(cand, ['--version']);
795
+ const m = stdout.match(/Python (\d+)\.(\d+)/);
796
+ if (m) {
797
+ pythonCmd = cand;
798
+ pythonVersion = `${m[1]}.${m[2]}`;
799
+ if (parseInt(m[1], 10) === 3 && parseInt(m[2], 10) >= 10) break;
800
+ }
801
+ } catch {}
802
+ }
803
+ const pyOk = pythonVersion && parseInt(pythonVersion.split('.')[0], 10) === 3 && parseInt(pythonVersion.split('.')[1], 10) >= 10;
804
+ checks.push({
805
+ name: 'Python ≥ 3.10',
806
+ status: pyOk ? 'pass' : 'fail',
807
+ detail: pythonVersion ? `${pythonCmd} ${pythonVersion}` : 'not found',
808
+ fix: pyOk ? null : 'Install Python 3.10+: https://www.python.org/downloads/',
809
+ });
810
+
811
+ let torchOk = false, torchDetail = 'not importable';
812
+ if (pythonCmd) {
813
+ try {
814
+ const { stdout } = await execFileAsync(pythonCmd, ['-c', 'import torch; print(torch.__version__)']);
815
+ torchOk = true;
816
+ torchDetail = `torch ${stdout.trim()}`;
817
+ } catch {}
818
+ }
819
+ checks.push({
820
+ name: 'PyTorch',
821
+ status: torchOk ? 'pass' : 'fail',
822
+ detail: torchDetail,
823
+ fix: torchOk ? null : `${pythonCmd || 'pip3'} -m pip install torch torchaudio`,
824
+ });
825
+
826
+ let ttsOk = false, ttsDetail = 'not importable';
827
+ if (pythonCmd) {
828
+ try {
829
+ const { stdout } = await execFileAsync(pythonCmd, ['-c', 'import TTS; print(TTS.__version__)']);
830
+ ttsOk = true;
831
+ ttsDetail = `TTS ${stdout.trim()}`;
832
+ } catch {}
833
+ }
834
+ checks.push({
835
+ name: 'TTS (XTTS v2)',
836
+ status: ttsOk ? 'pass' : 'fail',
837
+ detail: ttsDetail,
838
+ fix: ttsOk ? null : `${pythonCmd || 'pip3'} -m pip install TTS`,
839
+ });
840
+
841
+ let accel = 'cpu';
842
+ if (pythonCmd && torchOk) {
843
+ try {
844
+ const { stdout } = await execFileAsync(pythonCmd, ['-c',
845
+ 'import torch; print("mps" if torch.backends.mps.is_available() else ("cuda" if torch.cuda.is_available() else "cpu"))']);
846
+ accel = stdout.trim();
847
+ } catch {}
848
+ }
849
+ checks.push({
850
+ name: 'Acceleration',
851
+ status: accel === 'cpu' ? 'warn' : 'pass',
852
+ detail: accel.toUpperCase(),
853
+ fix: accel === 'cpu' ? 'CPU works but is slow. M-series/NVIDIA recommended.' : null,
854
+ });
855
+
856
+ const home = os.homedir();
857
+ const skillPath = path.join(home, '.claude', 'skills', 'voicci', 'SKILL.md');
858
+ const skillExists = fs.existsSync(skillPath);
859
+ checks.push({
860
+ name: 'Claude Code skill',
861
+ status: skillExists ? 'pass' : 'warn',
862
+ detail: skillExists ? skillPath : 'not installed (harmless if you do not use Claude Code)',
863
+ fix: skillExists ? null : 'Re-run: npm install -g voicci',
864
+ });
865
+
866
+ return {
867
+ voicciVersion: pkg.version,
868
+ platform: process.platform,
869
+ arch: process.arch,
870
+ checks,
871
+ };
872
+ }
873
+
874
+ function printDoctorReport(results) {
875
+ console.log('\n🩺 Voicci Doctor\n');
876
+ console.log(` voicci@${results.voicciVersion} · ${results.platform}/${results.arch}\n`);
877
+ const pad = (s, n) => (s + ' '.repeat(n)).slice(0, n);
878
+ for (const c of results.checks) {
879
+ const icon = c.status === 'pass' ? '✅' : c.status === 'warn' ? '⚠️ ' : '❌';
880
+ console.log(` ${icon} ${pad(c.name, 22)} ${c.detail}`);
881
+ if (c.fix) console.log(` ↳ ${c.fix}`);
882
+ }
883
+ const failed = results.checks.filter(c => c.status === 'fail');
884
+ console.log('');
885
+ if (failed.length === 0) {
886
+ console.log(' 🎉 All required prerequisites look good. You are ready to generate audiobooks.\n');
887
+ } else {
888
+ console.log(` ${failed.length} check(s) failed. Fix them and re-run \`voicci doctor\`.\n`);
889
+ }
890
+ }
891
+
724
892
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicci",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "AI-Powered Audiobook Generator for Claude Code, OpenCode & AI Code Editors. Convert books and PDFs to audiobooks using natural language.",
5
5
  "type": "module",
6
6
  "main": "cli/index.js",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "start": "node cli/index.js",
12
12
  "worker": "node backend/worker.js",
13
- "test": "node tests/test-cleaner.js",
13
+ "test": "node tests/test-cleaner.js && node tests/test-security.js",
14
14
  "postinstall": "node scripts/postinstall.js"
15
15
  },
16
16
  "keywords": [
@@ -22,7 +22,7 @@
22
22
  "text-to-speech",
23
23
  "cli",
24
24
  "pdf",
25
- "epub",
25
+ "book",
26
26
  "summarization",
27
27
  "claude-code",
28
28
  "claude-skill",
@@ -39,10 +39,10 @@
39
39
  "license": "MIT",
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/OlmiVanguard/voicci-cli.git"
42
+ "url": "https://github.com/daniel-mf-92/voicci-cli.git"
43
43
  },
44
44
  "bugs": {
45
- "url": "https://github.com/OlmiVanguard/voicci-cli/issues"
45
+ "url": "https://github.com/daniel-mf-92/voicci-cli/issues"
46
46
  },
47
47
  "homepage": "https://voicci.com/voicci-cli",
48
48
  "dependencies": {
@@ -9,12 +9,13 @@ import { execFileSync } from 'child_process';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
11
 
12
- // Two skill files: simple and detailed
13
- const SKILL_NAME_SIMPLE = 'voicci.md';
14
- const SKILL_NAME_DETAILED = 'voicci-audiobook.md';
15
-
16
- const SKILL_CONTENT_SIMPLE = `---
17
- description: "Voicci - AI audiobook generator"
12
+ // Skills use directory-based format: skills/<name>/SKILL.md
13
+ const SKILLS = [
14
+ {
15
+ dirName: 'voicci',
16
+ content: `---
17
+ name: voicci
18
+ description: "Voicci - AI audiobook generator. Use when the user wants to convert books, PDFs, or documents to audiobooks."
18
19
  argument-hint: "COMMAND_OR_FILE"
19
20
  ---
20
21
 
@@ -33,10 +34,13 @@ Convert books, PDFs, and documents to audiobooks using AI text-to-speech.
33
34
  #!/bin/bash
34
35
  voicci $ARGUMENTS
35
36
  \`\`\`
36
- `;
37
-
38
- const SKILL_CONTENT_DETAILED = `---
39
- description: "Voicci - AI audiobook generator CLI"
37
+ `
38
+ },
39
+ {
40
+ dirName: 'voicci-audiobook',
41
+ content: `---
42
+ name: voicci-audiobook
43
+ description: "Voicci - AI audiobook generator with full documentation. Use when the user needs detailed help with audiobook generation."
40
44
  argument-hint: "COMMAND_OR_FILE"
41
45
  ---
42
46
 
@@ -101,7 +105,6 @@ voicci memory
101
105
 
102
106
  \`\`\`!
103
107
  #!/bin/bash
104
- # Execute voicci with user-provided arguments
105
108
  voicci $ARGUMENTS
106
109
  \`\`\`
107
110
 
@@ -112,14 +115,9 @@ voicci $ARGUMENTS
112
115
  - macOS: \`~/Library/Application Support/voicci/audiobooks/\`
113
116
  - Linux: \`~/.local/share/voicci/audiobooks/\`
114
117
  - Copyright warning will appear before first book search
115
-
116
- ## Usage Examples
117
- - \`/voicci-audiobook "Lord of the Rings"\` - Search and convert book
118
- - \`/voicci-audiobook ~/Documents/book.pdf\` - Convert local file
119
- - \`/voicci-audiobook -s\` - Check all job statuses
120
- - \`/voicci-audiobook summary book.pdf\` - Generate summary only
121
- - \`/voicci-audiobook --help\` - Show all options
122
- `;
118
+ `
119
+ }
120
+ ];
123
121
 
124
122
  // Editor detection configurations
125
123
  const EDITORS = {
@@ -127,7 +125,6 @@ const EDITORS = {
127
125
  name: 'Claude Code',
128
126
  detect: () => {
129
127
  const homeDir = os.homedir();
130
- // Check for claude command or .claude directory
131
128
  if (fs.existsSync(path.join(homeDir, '.claude'))) return true;
132
129
  try {
133
130
  execFileSync('which', ['claude'], { stdio: 'ignore' });
@@ -136,11 +133,7 @@ const EDITORS = {
136
133
  return false;
137
134
  }
138
135
  },
139
- // Install to both skills/ and commands/ so slash commands appear in autocomplete
140
- skillsDirs: () => [
141
- path.join(os.homedir(), '.claude', 'commands'),
142
- path.join(os.homedir(), '.claude', 'skills')
143
- ]
136
+ skillsDir: () => path.join(os.homedir(), '.claude', 'skills')
144
137
  },
145
138
  'OpenCode': {
146
139
  name: 'OpenCode',
@@ -154,7 +147,7 @@ const EDITORS = {
154
147
  return false;
155
148
  }
156
149
  },
157
- skillsDirs: () => [path.join(os.homedir(), '.opencode', 'skills')]
150
+ skillsDir: () => path.join(os.homedir(), '.opencode', 'skills')
158
151
  },
159
152
  'Cursor': {
160
153
  name: 'Cursor',
@@ -169,17 +162,16 @@ const EDITORS = {
169
162
  return false;
170
163
  }
171
164
  },
172
- skillsDirs: () => {
165
+ skillsDir: () => {
173
166
  const homeDir = os.homedir();
174
167
  const locations = [
175
168
  path.join(homeDir, '.cursor', 'skills'),
176
169
  path.join(homeDir, 'Library', 'Application Support', 'Cursor', 'skills')
177
170
  ];
178
171
  for (const loc of locations) {
179
- const parent = path.dirname(loc);
180
- if (fs.existsSync(parent)) return [loc];
172
+ if (fs.existsSync(path.dirname(loc))) return loc;
181
173
  }
182
- return [locations[0]];
174
+ return locations[0];
183
175
  }
184
176
  },
185
177
  'Windsurf': {
@@ -194,64 +186,65 @@ const EDITORS = {
194
186
  return false;
195
187
  }
196
188
  },
197
- skillsDirs: () => [path.join(os.homedir(), '.windsurf', 'skills')]
189
+ skillsDir: () => path.join(os.homedir(), '.windsurf', 'skills')
198
190
  }
199
191
  };
200
192
 
201
- function installSkillFile(skillsDir, skillName, content) {
202
- const skillFile = path.join(skillsDir, skillName);
203
-
193
+ function installSkill(skillsDir, skill) {
194
+ const skillDir = path.join(skillsDir, skill.dirName);
195
+ const skillFile = path.join(skillDir, 'SKILL.md');
196
+
204
197
  // Check if already installed
205
198
  if (fs.existsSync(skillFile)) {
206
199
  const existingContent = fs.readFileSync(skillFile, 'utf8');
207
- if (existingContent.includes('voicci $ARGUMENTS')) {
200
+ if (existingContent.includes('voicci')) {
208
201
  return 'exists';
209
202
  }
210
203
  }
211
-
212
- // Write the skill file
213
- fs.writeFileSync(skillFile, content, 'utf8');
204
+
205
+ // Create skill directory and write SKILL.md
206
+ fs.mkdirSync(skillDir, { recursive: true });
207
+ fs.writeFileSync(skillFile, skill.content, 'utf8');
208
+
209
+ // Clean up legacy flat file if it exists
210
+ const legacyFile = path.join(skillsDir, `${skill.dirName}.md`);
211
+ if (fs.existsSync(legacyFile)) {
212
+ fs.unlinkSync(legacyFile);
213
+ }
214
+
214
215
  return 'new';
215
216
  }
216
217
 
217
218
  function detectAndInstall() {
218
- const homeDir = os.homedir();
219
219
  const installedEditors = [];
220
220
  const failedEditors = [];
221
221
 
222
- console.log('🔍 Detecting AI code editors...\n');
222
+ console.log('Detecting AI code editors...\n');
223
223
 
224
- // Detect and install for each editor
225
224
  for (const [key, editor] of Object.entries(EDITORS)) {
226
225
  try {
227
226
  if (editor.detect()) {
228
- console.log(`✓ Found ${editor.name}`);
227
+ console.log(` Found ${editor.name}`);
229
228
 
230
- const dirs = editor.skillsDirs();
231
-
232
- let anyNew = false;
233
- let allExist = true;
229
+ const skillsDir = editor.skillsDir();
234
230
 
235
- for (const skillsDir of dirs) {
236
- // Create directory if it doesn't exist
237
- if (!fs.existsSync(skillsDir)) {
238
- fs.mkdirSync(skillsDir, { recursive: true });
239
- }
231
+ // Create skills directory if needed
232
+ if (!fs.existsSync(skillsDir)) {
233
+ fs.mkdirSync(skillsDir, { recursive: true });
234
+ }
240
235
 
241
- // Install both skills
242
- const simpleStatus = installSkillFile(skillsDir, SKILL_NAME_SIMPLE, SKILL_CONTENT_SIMPLE);
243
- const detailedStatus = installSkillFile(skillsDir, SKILL_NAME_DETAILED, SKILL_CONTENT_DETAILED);
236
+ let anyNew = false;
244
237
 
245
- if (simpleStatus === 'new' || detailedStatus === 'new') {
238
+ for (const skill of SKILLS) {
239
+ const status = installSkill(skillsDir, skill);
240
+ if (status === 'new') {
246
241
  anyNew = true;
247
- allExist = false;
248
- if (simpleStatus === 'new') console.log(` ↳ Installed: ${path.join(skillsDir, SKILL_NAME_SIMPLE)}`);
249
- if (detailedStatus === 'new') console.log(` ↳ Installed: ${path.join(skillsDir, SKILL_NAME_DETAILED)}`);
242
+ console.log(` Installed: ${path.join(skillsDir, skill.dirName, 'SKILL.md')}`);
250
243
  }
251
244
  }
252
245
 
253
246
  if (!anyNew) {
254
- console.log(`Skills already installed`);
247
+ console.log(` Skills already installed`);
255
248
  installedEditors.push({ name: editor.name, status: 'exists' });
256
249
  } else {
257
250
  installedEditors.push({ name: editor.name, status: 'new' });
@@ -263,37 +256,36 @@ function detectAndInstall() {
263
256
  }
264
257
 
265
258
  // Summary
266
- console.log('\n' + ''.repeat(60));
259
+ console.log('\n' + '='.repeat(60));
267
260
 
268
261
  if (installedEditors.length > 0) {
269
- console.log('\n✅ Voicci CLI installed successfully!');
270
- console.log('\n📦 Command-line tool: voicci');
271
- console.log('🔧 Skill commands: /voicci OR /voicci-audiobook');
272
- console.log('\n📍 Installed in:');
262
+ console.log('\nVoicci CLI installed successfully!');
263
+ console.log('\n Command-line tool: voicci');
264
+ console.log(' Skill commands: /voicci OR /voicci-audiobook');
265
+ console.log('\n Installed in:');
273
266
  installedEditors.forEach(editor => {
274
267
  const status = editor.status === 'new' ? '(new)' : '(already installed)';
275
- console.log(`${editor.name} ${status}`);
268
+ console.log(` ${editor.name} ${status}`);
276
269
  });
277
270
 
278
- console.log('\n💡 Usage:');
279
- console.log(' 1. Restart your AI code editor');
280
- console.log(' 2. Use: /voicci "search query" (simple)');
281
- console.log(' 3. Or: /voicci-audiobook "search query" (detailed docs)');
282
- console.log(' 4. Or CLI: voicci "your search query"');
271
+ console.log('\n Usage:');
272
+ console.log(' 1. Restart your AI code editor');
273
+ console.log(' 2. Type: /voicci "search query"');
274
+ console.log(' 3. Or CLI: voicci "your search query"');
283
275
  } else {
284
- console.log('\n⚠️ No supported AI code editors detected');
285
- console.log('\nSupported editors: Claude Code, OpenCode, Cursor, Windsurf');
286
- console.log('\nYou can still use the CLI: voicci <command>');
276
+ console.log('\n No supported AI code editors detected');
277
+ console.log('\n Supported editors: Claude Code, OpenCode, Cursor, Windsurf');
278
+ console.log('\n You can still use the CLI: voicci <command>');
287
279
  }
288
280
 
289
281
  if (failedEditors.length > 0) {
290
- console.log('\n⚠️ Failed to install for:');
282
+ console.log('\n Failed to install for:');
291
283
  failedEditors.forEach(({ name, error }) => {
292
- console.log(`${name}: ${error}`);
284
+ console.log(` ${name}: ${error}`);
293
285
  });
294
286
  }
295
287
 
296
- console.log('\n' + ''.repeat(60));
288
+ console.log('\n' + '='.repeat(60));
297
289
  console.log();
298
290
  }
299
291