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 +17 -1
- package/cli/index.js +183 -15
- package/package.json +5 -5
- package/scripts/postinstall.js +68 -76
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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/
|
|
42
|
+
"url": "https://github.com/daniel-mf-92/voicci-cli.git"
|
|
43
43
|
},
|
|
44
44
|
"bugs": {
|
|
45
|
-
"url": "https://github.com/
|
|
45
|
+
"url": "https://github.com/daniel-mf-92/voicci-cli/issues"
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://voicci.com/voicci-cli",
|
|
48
48
|
"dependencies": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
//
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
if (fs.existsSync(parent)) return [loc];
|
|
172
|
+
if (fs.existsSync(path.dirname(loc))) return loc;
|
|
181
173
|
}
|
|
182
|
-
return
|
|
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
|
-
|
|
189
|
+
skillsDir: () => path.join(os.homedir(), '.windsurf', 'skills')
|
|
198
190
|
}
|
|
199
191
|
};
|
|
200
192
|
|
|
201
|
-
function
|
|
202
|
-
const
|
|
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
|
|
200
|
+
if (existingContent.includes('voicci')) {
|
|
208
201
|
return 'exists';
|
|
209
202
|
}
|
|
210
203
|
}
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
fs.
|
|
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('
|
|
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(
|
|
227
|
+
console.log(` Found ${editor.name}`);
|
|
229
228
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
let anyNew = false;
|
|
233
|
-
let allExist = true;
|
|
229
|
+
const skillsDir = editor.skillsDir();
|
|
234
230
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
231
|
+
// Create skills directory if needed
|
|
232
|
+
if (!fs.existsSync(skillsDir)) {
|
|
233
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
234
|
+
}
|
|
240
235
|
|
|
241
|
-
|
|
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
|
-
|
|
238
|
+
for (const skill of SKILLS) {
|
|
239
|
+
const status = installSkill(skillsDir, skill);
|
|
240
|
+
if (status === 'new') {
|
|
246
241
|
anyNew = true;
|
|
247
|
-
|
|
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(`
|
|
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' + '
|
|
259
|
+
console.log('\n' + '='.repeat(60));
|
|
267
260
|
|
|
268
261
|
if (installedEditors.length > 0) {
|
|
269
|
-
console.log('\
|
|
270
|
-
console.log('\n
|
|
271
|
-
console.log('
|
|
272
|
-
console.log('\n
|
|
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(`
|
|
268
|
+
console.log(` ${editor.name} ${status}`);
|
|
276
269
|
});
|
|
277
270
|
|
|
278
|
-
console.log('\n
|
|
279
|
-
console.log('
|
|
280
|
-
console.log('
|
|
281
|
-
console.log('
|
|
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
|
|
285
|
-
console.log('\
|
|
286
|
-
console.log('\n
|
|
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
|
|
282
|
+
console.log('\n Failed to install for:');
|
|
291
283
|
failedEditors.forEach(({ name, error }) => {
|
|
292
|
-
console.log(`
|
|
284
|
+
console.log(` ${name}: ${error}`);
|
|
293
285
|
});
|
|
294
286
|
}
|
|
295
287
|
|
|
296
|
-
console.log('\n' + '
|
|
288
|
+
console.log('\n' + '='.repeat(60));
|
|
297
289
|
console.log();
|
|
298
290
|
}
|
|
299
291
|
|