agentvibes 5.6.9 → 5.7.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/.agentvibes/config.json +3 -38
- package/.claude/commands/agent-vibes/provider.md +0 -0
- package/.claude/config/audio-effects.cfg +1 -1
- package/.claude/config/background-music-position.txt +6 -8
- package/.claude/config/reverb-level.txt +0 -0
- package/.claude/github-star-reminder.txt +1 -1
- package/.claude/hooks/bmad-tts-injector.sh +49 -21
- package/.claude/hooks/migrate-to-agentvibes.sh +24 -16
- package/.claude/hooks/personality-manager.sh +15 -2
- package/.claude/hooks/play-tts.sh +6 -0
- package/.claude/hooks/provider-commands.sh +16 -4
- package/.claude/hooks/provider-manager.sh +38 -0
- package/.claude/hooks/stop.sh +2 -27
- package/.claude/hooks/voice-manager.sh +50 -2
- package/.claude/hooks-windows/play-tts.ps1 +34 -1
- package/.claude/hooks-windows/tts-watcher.ps1 +122 -0
- package/.claude/piper-voices-dir.txt +1 -1
- package/.mcp.json +13 -33
- package/README.md +6 -8
- package/RELEASE_NOTES.md +32 -0
- package/bin/agent-vibes +39 -39
- package/package.json +1 -1
- package/src/bmad-detector.js +85 -71
- package/src/cli/list-personalities.js +110 -110
- package/src/cli/list-voices.js +114 -114
- package/src/commands/bmad-voices.js +394 -394
- package/src/commands/install-mcp.js +476 -476
- package/src/console/brand-colors.js +13 -13
- package/src/console/constants/personalities.js +44 -44
- package/src/console/tabs/help-tab.js +314 -314
- package/src/console/tabs/readme-tab.js +272 -272
- package/src/console/widgets/destroy-list.js +25 -25
- package/src/console/widgets/notice.js +55 -55
- package/src/console/widgets/personality-picker.js +213 -213
- package/src/i18n/de.js +202 -202
- package/src/i18n/es.js +202 -202
- package/src/i18n/fr.js +202 -202
- package/src/i18n/hi.js +202 -202
- package/src/i18n/ja.js +202 -202
- package/src/i18n/ko.js +202 -202
- package/src/i18n/pt.js +202 -202
- package/src/i18n/strings.js +54 -54
- package/src/i18n/zh-CN.js +202 -202
- package/src/installer/language-screen.js +31 -31
- package/src/installer/music-file-input.js +304 -304
- package/src/installer.js +330 -64
- package/src/services/agent-voice-store.js +59 -12
- package/src/services/config-service.js +264 -264
- package/src/services/language-service.js +47 -47
- package/src/services/llm-provider-service.js +57 -12
- package/src/services/provider-service.js +143 -143
- package/src/utils/audio-duration-validator.js +298 -298
- package/src/utils/audio-format-validator.js +277 -277
- package/src/utils/dependency-checker.js +469 -469
- package/src/utils/file-ownership-verifier.js +358 -358
- package/src/utils/list-formatter.js +194 -194
- package/src/utils/music-file-validator.js +285 -285
- package/src/utils/preview-list-prompt.js +136 -136
- package/src/utils/secure-music-storage.js +412 -412
- package/.agentvibes/LITE-MODE.md +0 -236
- package/.agentvibes/README.md +0 -136
- package/.agentvibes/backup/session-start-tts.sh.20251210_212814 +0 -141
- package/.agentvibes/backups/agents/analyst_20260204_144958.md +0 -78
- package/.agentvibes/backups/agents/architect_20260204_144958.md +0 -72
- package/.agentvibes/backups/agents/dev_20260204_144958.md +0 -74
- package/.agentvibes/backups/agents/pm_20260204_144958.md +0 -72
- package/.agentvibes/backups/agents/quick-flow-solo-dev_20260204_144958.md +0 -64
- package/.agentvibes/backups/agents/sm_20260204_144958.md +0 -87
- package/.agentvibes/backups/agents/tea_20260204_144958.md +0 -79
- package/.agentvibes/backups/agents/tech-writer_20260204_144958.md +0 -82
- package/.agentvibes/backups/agents/ux-designer_20260204_144958.md +0 -80
- package/.agentvibes/config/README-personality-defaults.md +0 -162
- package/.agentvibes/config/agentvibes.json +0 -1
- package/.agentvibes/config/mode.txt +0 -1
- package/.agentvibes/config/personality-voice-defaults.default.json +0 -21
- package/.agentvibes/config/save-audio.txt +0 -1
- package/.agentvibes/config/voice-metadata.json +0 -160
- package/.agentvibes/hooks/help.sh +0 -191
- package/.agentvibes/hooks/post-tool-use-lite.sh +0 -111
- package/.agentvibes/hooks/save-audio-manager.sh +0 -162
- package/.agentvibes/hooks/session-start-full-optimized.sh +0 -102
- package/.agentvibes/hooks/session-start-full.sh +0 -142
- package/.agentvibes/hooks/session-start-lite-v2.sh +0 -34
- package/.agentvibes/hooks/session-start-lite.sh +0 -29
- package/.agentvibes/hooks/stop-lite.sh +0 -115
- package/.agentvibes/hooks/switch-mode.sh +0 -215
- package/.agentvibes/output-styles/audio-summary.md +0 -30
- package/.claude/audio/voice-samples/piper/alan.wav +0 -0
- package/.claude/audio/voice-samples/piper/amy.wav +0 -0
- package/.claude/audio/voice-samples/piper/charlotte.wav +0 -0
- package/.claude/audio/voice-samples/piper/joe.wav +0 -0
- package/.claude/audio/voice-samples/piper/john.wav +0 -0
- package/.claude/audio/voice-samples/piper/katherine.wav +0 -0
- package/.claude/audio/voice-samples/piper/kristin.wav +0 -0
- package/.claude/audio/voice-samples/piper/linda.wav +0 -0
- package/.claude/audio/voice-samples/piper/marcus.wav +0 -0
- package/.claude/audio/voice-samples/piper/ryan.wav +0 -0
- package/.claude/hooks/post-response.sh +0 -41
- package/bin/ensure-soprano-running.sh +0 -43
package/src/installer.js
CHANGED
|
@@ -84,6 +84,105 @@ const packageJson = JSON.parse(
|
|
|
84
84
|
);
|
|
85
85
|
const VERSION = packageJson.version;
|
|
86
86
|
|
|
87
|
+
// ── Install manifest utilities ──────────────────────────────────────────────
|
|
88
|
+
// The manifest records the SHA-256 hash of every file AgentVibes installs,
|
|
89
|
+
// so update() can detect user modifications before overwriting. Files whose
|
|
90
|
+
// hash has changed since the last install are skipped (a .user.bak copy is
|
|
91
|
+
// saved), preventing silent destruction of user customisations.
|
|
92
|
+
|
|
93
|
+
const MANIFEST_FILENAME = 'install-manifest.json';
|
|
94
|
+
|
|
95
|
+
function getProjectManifestPath(targetDir) {
|
|
96
|
+
return path.join(targetDir, '.agentvibes', MANIFEST_FILENAME);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getGlobalManifestPath(homeDir) {
|
|
100
|
+
return path.join(homeDir || os.homedir(), '.agentvibes', MANIFEST_FILENAME);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function computeFileHash(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const buf = await fs.readFile(filePath);
|
|
106
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadManifest(manifestPath) {
|
|
113
|
+
try {
|
|
114
|
+
const data = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
115
|
+
return data.files ?? {};
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function saveManifest(manifestPath, files) {
|
|
122
|
+
await fs.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
123
|
+
const tmp = manifestPath + '.tmp.' + process.pid;
|
|
124
|
+
try {
|
|
125
|
+
await fs.writeFile(
|
|
126
|
+
tmp,
|
|
127
|
+
JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), files }, null, 2),
|
|
128
|
+
{ mode: 0o600 }
|
|
129
|
+
);
|
|
130
|
+
await fs.rename(tmp, manifestPath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
try { await fs.unlink(tmp); } catch { /* cleanup */ }
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Copies srcPath → destPath only when safe:
|
|
138
|
+
// • dest missing → copy (action: 'new')
|
|
139
|
+
// • dest == src (already current) → skip (action: 'unchanged')
|
|
140
|
+
// • dest hash == manifest hash → copy (action: 'updated')
|
|
141
|
+
// • dest hash != manifest hash → skip (action: 'skipped', .user.bak saved)
|
|
142
|
+
// • no manifest entry yet → copy (action: 'updated')
|
|
143
|
+
async function manifestSafeCopy(srcPath, destPath, manifest) {
|
|
144
|
+
const srcHash = await computeFileHash(srcPath);
|
|
145
|
+
if (!srcHash) return { action: 'skipped', hash: null }; // src missing
|
|
146
|
+
|
|
147
|
+
const destHash = await computeFileHash(destPath);
|
|
148
|
+
|
|
149
|
+
if (!destHash) {
|
|
150
|
+
await fs.copyFile(srcPath, destPath);
|
|
151
|
+
return { action: 'new', hash: srcHash };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (destHash === srcHash) {
|
|
155
|
+
return { action: 'unchanged', hash: srcHash };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const manifestHash = manifest[destPath]?.hash;
|
|
159
|
+
if (manifestHash && destHash !== manifestHash) {
|
|
160
|
+
// User modified the file since we last installed it — preserve it
|
|
161
|
+
try { await fs.copyFile(destPath, destPath + '.user.bak'); } catch { /* best effort */ }
|
|
162
|
+
return { action: 'skipped', hash: destHash };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Stock file (or no manifest entry yet) — safe to overwrite
|
|
166
|
+
await fs.copyFile(srcPath, destPath);
|
|
167
|
+
return { action: 'updated', hash: srcHash };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Delete only the files listed in manifest that reside under baseDir.
|
|
171
|
+
// Then attempt (non-recursively) to prune any resulting empty directories.
|
|
172
|
+
async function removeManifestFiles(manifest, baseDir, dirsToTryPrune = []) {
|
|
173
|
+
const base = path.resolve(baseDir);
|
|
174
|
+
let count = 0;
|
|
175
|
+
for (const filePath of Object.keys(manifest)) {
|
|
176
|
+
const resolved = path.resolve(filePath);
|
|
177
|
+
if (!resolved.startsWith(base + path.sep)) continue;
|
|
178
|
+
try { await fs.unlink(filePath); count++; } catch { /* already gone */ }
|
|
179
|
+
}
|
|
180
|
+
for (const dir of dirsToTryPrune) {
|
|
181
|
+
try { await fs.rmdir(dir); } catch { /* not empty or already gone — leave it */ }
|
|
182
|
+
}
|
|
183
|
+
return count;
|
|
184
|
+
}
|
|
185
|
+
|
|
87
186
|
// Personality emoji mapping for quick visual recognition
|
|
88
187
|
const personalityEmojis = {
|
|
89
188
|
'angry': '😠',
|
|
@@ -3387,19 +3486,30 @@ async function copyCommandFiles(targetDir, spinner) {
|
|
|
3387
3486
|
let failedCommands = [];
|
|
3388
3487
|
let successCount = 0;
|
|
3389
3488
|
|
|
3489
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
3490
|
+
const manifest = await loadManifest(manifestPath);
|
|
3491
|
+
const manifestUpdates = {};
|
|
3492
|
+
|
|
3390
3493
|
for (const file of commandFiles) {
|
|
3391
3494
|
const srcPath = path.join(srcCommandsDir, file);
|
|
3392
3495
|
const destPath = path.join(agentVibesCommandsDir, file);
|
|
3393
3496
|
try {
|
|
3394
|
-
await
|
|
3395
|
-
|
|
3396
|
-
|
|
3497
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
3498
|
+
if (result.action !== 'skipped') {
|
|
3499
|
+
installedCommands.push(file);
|
|
3500
|
+
successCount++;
|
|
3501
|
+
if (result.hash) manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
3502
|
+
}
|
|
3397
3503
|
} catch (err) {
|
|
3398
3504
|
failedCommands.push({ file, error: err.message });
|
|
3399
3505
|
// Continue with other files
|
|
3400
3506
|
}
|
|
3401
3507
|
}
|
|
3402
3508
|
|
|
3509
|
+
if (Object.keys(manifestUpdates).length > 0) {
|
|
3510
|
+
await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3403
3513
|
if (successCount === commandFiles.length) {
|
|
3404
3514
|
spinner.succeed(chalk.green(`Installed ${successCount} slash commands!\n`));
|
|
3405
3515
|
} else {
|
|
@@ -3515,16 +3625,17 @@ async function filterHookFiles(srcHooksDir, allFiles) {
|
|
|
3515
3625
|
* @param {string} filename - Name of the file
|
|
3516
3626
|
* @returns {Promise<Object>} Result object with success/error info
|
|
3517
3627
|
*/
|
|
3518
|
-
async function copyHookFile(srcPath, destPath, filename) {
|
|
3628
|
+
async function copyHookFile(srcPath, destPath, filename, manifest = {}) {
|
|
3519
3629
|
try {
|
|
3520
|
-
await
|
|
3521
|
-
|
|
3522
|
-
|
|
3630
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
3631
|
+
if (result.action === 'skipped' && result.hash !== null) {
|
|
3632
|
+
// User-modified file preserved — do not chmod or report as installed
|
|
3633
|
+
return { success: true, name: filename, executable: false, userModified: true, hash: null };
|
|
3634
|
+
}
|
|
3635
|
+
if (filename.endsWith('.sh') && result.action !== 'skipped') {
|
|
3523
3636
|
await fs.chmod(destPath, 0o750);
|
|
3524
|
-
return { success: true, name: filename, executable: true };
|
|
3525
3637
|
}
|
|
3526
|
-
|
|
3527
|
-
return { success: true, name: filename, executable: false };
|
|
3638
|
+
return { success: true, name: filename, executable: filename.endsWith('.sh'), hash: result.hash };
|
|
3528
3639
|
} catch (err) {
|
|
3529
3640
|
return { success: false, name: filename, error: err.message };
|
|
3530
3641
|
}
|
|
@@ -3600,21 +3711,33 @@ async function copyHookFiles(targetDir, spinner) {
|
|
|
3600
3711
|
|
|
3601
3712
|
spinner.start(`Installing ${hookFiles.length} TTS scripts...`);
|
|
3602
3713
|
|
|
3714
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
3715
|
+
const manifest = await loadManifest(manifestPath);
|
|
3716
|
+
const manifestUpdates = {};
|
|
3717
|
+
|
|
3603
3718
|
const installedFiles = [];
|
|
3604
3719
|
const failedFiles = [];
|
|
3605
3720
|
|
|
3606
3721
|
for (const file of hookFiles) {
|
|
3607
3722
|
const srcPath = path.join(srcHooksDir, file);
|
|
3608
3723
|
const destPath = path.join(hooksDir, file);
|
|
3609
|
-
const result = await copyHookFile(srcPath, destPath, file);
|
|
3724
|
+
const result = await copyHookFile(srcPath, destPath, file, manifest);
|
|
3610
3725
|
|
|
3611
3726
|
if (result.success) {
|
|
3612
|
-
|
|
3727
|
+
if (!result.userModified) {
|
|
3728
|
+
installedFiles.push({ name: result.name, executable: result.executable });
|
|
3729
|
+
if (result.hash) manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
3730
|
+
}
|
|
3731
|
+
// userModified files are silently skipped (their .user.bak is already saved)
|
|
3613
3732
|
} else {
|
|
3614
3733
|
failedFiles.push({ name: result.name, error: result.error });
|
|
3615
3734
|
}
|
|
3616
3735
|
}
|
|
3617
3736
|
|
|
3737
|
+
if (Object.keys(manifestUpdates).length > 0) {
|
|
3738
|
+
await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3618
3741
|
const successCount = installedFiles.length;
|
|
3619
3742
|
|
|
3620
3743
|
if (successCount === hookFiles.length) {
|
|
@@ -3663,11 +3786,22 @@ async function copyPersonalityFiles(targetDir, spinner) {
|
|
|
3663
3786
|
spinner.start(`Installing ${personalityMdFiles.length} personality templates...`);
|
|
3664
3787
|
let installedPersonalities = [];
|
|
3665
3788
|
|
|
3789
|
+
const personalityManifestPath = getProjectManifestPath(targetDir);
|
|
3790
|
+
const personalityManifest = await loadManifest(personalityManifestPath);
|
|
3791
|
+
const personalityManifestUpdates = {};
|
|
3792
|
+
|
|
3666
3793
|
for (const file of personalityMdFiles) {
|
|
3667
3794
|
const srcPath = path.join(srcPersonalitiesDir, file);
|
|
3668
3795
|
const destPath = path.join(destPersonalitiesDir, file);
|
|
3669
|
-
await
|
|
3670
|
-
|
|
3796
|
+
const result = await manifestSafeCopy(srcPath, destPath, personalityManifest);
|
|
3797
|
+
if (result.action !== 'skipped') {
|
|
3798
|
+
installedPersonalities.push(file);
|
|
3799
|
+
if (result.hash) personalityManifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
if (Object.keys(personalityManifestUpdates).length > 0) {
|
|
3804
|
+
await saveManifest(personalityManifestPath, { ...personalityManifest, ...personalityManifestUpdates }).catch(() => { /* best effort */ });
|
|
3671
3805
|
}
|
|
3672
3806
|
|
|
3673
3807
|
spinner.succeed(chalk.green('Installed personality templates!\n'));
|
|
@@ -4570,9 +4704,8 @@ async function handleMcpConfiguration(targetDir, options) {
|
|
|
4570
4704
|
* @param {string} targetDir - Base installation directory to validate bmadPath is within
|
|
4571
4705
|
*/
|
|
4572
4706
|
async function processBmadTtsInjections(bmadPath, targetDir) {
|
|
4573
|
-
// Security:
|
|
4574
|
-
|
|
4575
|
-
if (!isPathSafe(bmadPath, targetDir)) {
|
|
4707
|
+
// Security: bmadPath must be within targetDir OR home dir (BMAD may be installed globally at ~/_bmad)
|
|
4708
|
+
if (!isPathSafe(bmadPath, targetDir) && !isPathSafe(bmadPath, os.homedir())) {
|
|
4576
4709
|
console.error(chalk.red('⚠️ Security: Invalid BMAD path detected'));
|
|
4577
4710
|
return;
|
|
4578
4711
|
}
|
|
@@ -4841,14 +4974,17 @@ async function handleBmadIntegration(targetDir, options = {}) {
|
|
|
4841
4974
|
}
|
|
4842
4975
|
|
|
4843
4976
|
// Process TTS_INJECTION markers in BMAD files if they exist
|
|
4844
|
-
//
|
|
4845
|
-
|
|
4977
|
+
// Skip for global home-dir BMAD installs — only inject into project-local BMAD
|
|
4978
|
+
if (!bmadDetection.isGlobal) {
|
|
4979
|
+
await processBmadTtsInjections(bmadDetection.bmadPath, targetDir);
|
|
4980
|
+
}
|
|
4846
4981
|
|
|
4847
4982
|
// Create default voice assignments if they don't exist
|
|
4848
4983
|
await createDefaultBmadVoiceAssignmentsProactive(targetDir);
|
|
4849
4984
|
|
|
4850
4985
|
// Prompt user to inject TTS into BMAD agents (or auto-inject with --yes flag)
|
|
4851
|
-
|
|
4986
|
+
// Skip injection prompt for global BMAD — modifying shared ~/.bmad files from a project install is wrong
|
|
4987
|
+
let enableTtsInjection = bmadDetection.isGlobal ? false : options.yes;
|
|
4852
4988
|
|
|
4853
4989
|
if (!options.yes) {
|
|
4854
4990
|
console.log(''); // Add spacing
|
|
@@ -4980,6 +5116,10 @@ async function updatePersonalityFiles(targetDir, srcPersonalitiesDir) {
|
|
|
4980
5116
|
let newPersonalities = 0;
|
|
4981
5117
|
let updatedPersonalities = 0;
|
|
4982
5118
|
|
|
5119
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
5120
|
+
const manifest = await loadManifest(manifestPath);
|
|
5121
|
+
const manifestUpdates = {};
|
|
5122
|
+
|
|
4983
5123
|
for (const file of allPersonalityFiles) {
|
|
4984
5124
|
const srcPath = path.join(srcPersonalitiesDir, file);
|
|
4985
5125
|
const stat = await fs.stat(srcPath);
|
|
@@ -4989,17 +5129,24 @@ async function updatePersonalityFiles(targetDir, srcPersonalitiesDir) {
|
|
|
4989
5129
|
}
|
|
4990
5130
|
|
|
4991
5131
|
const destPath = path.join(destPersonalitiesDir, file);
|
|
5132
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
4992
5133
|
|
|
4993
|
-
|
|
4994
|
-
await fs.access(destPath);
|
|
4995
|
-
await fs.copyFile(srcPath, destPath);
|
|
4996
|
-
updatedPersonalities++;
|
|
4997
|
-
} catch {
|
|
4998
|
-
await fs.copyFile(srcPath, destPath);
|
|
5134
|
+
if (result.action === 'new') {
|
|
4999
5135
|
newPersonalities++;
|
|
5136
|
+
} else if (result.action === 'updated' || result.action === 'unchanged') {
|
|
5137
|
+
updatedPersonalities++;
|
|
5138
|
+
}
|
|
5139
|
+
// 'skipped' = user modified — leave it alone, .user.bak already saved
|
|
5140
|
+
|
|
5141
|
+
if (result.hash && result.action !== 'skipped') {
|
|
5142
|
+
manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
5000
5143
|
}
|
|
5001
5144
|
}
|
|
5002
5145
|
|
|
5146
|
+
if (Object.keys(manifestUpdates).length > 0) {
|
|
5147
|
+
await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
|
|
5148
|
+
}
|
|
5149
|
+
|
|
5003
5150
|
return { new: newPersonalities, updated: updatedPersonalities };
|
|
5004
5151
|
}
|
|
5005
5152
|
|
|
@@ -5031,10 +5178,21 @@ async function updateCommandFiles(targetDir, spinner) {
|
|
|
5031
5178
|
const srcCommandsDir = path.join(__dirname, '..', '.claude', 'commands', 'agent-vibes');
|
|
5032
5179
|
const commandFiles = await fs.readdir(srcCommandsDir);
|
|
5033
5180
|
|
|
5181
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
5182
|
+
const manifest = await loadManifest(manifestPath);
|
|
5183
|
+
const manifestUpdates = {};
|
|
5184
|
+
|
|
5034
5185
|
for (const file of commandFiles) {
|
|
5035
5186
|
const srcPath = path.join(srcCommandsDir, file);
|
|
5036
5187
|
const destPath = path.join(commandsDir, file);
|
|
5037
|
-
await
|
|
5188
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
5189
|
+
if (result.hash && result.action !== 'skipped') {
|
|
5190
|
+
manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
if (Object.keys(manifestUpdates).length > 0) {
|
|
5195
|
+
await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
|
|
5038
5196
|
}
|
|
5039
5197
|
|
|
5040
5198
|
return commandFiles.length;
|
|
@@ -5047,7 +5205,7 @@ async function updateCommandFiles(targetDir, spinner) {
|
|
|
5047
5205
|
* on every `npx agentvibes update` regardless of target directory.
|
|
5048
5206
|
*/
|
|
5049
5207
|
const CRITICAL_HOOKS = ['stop-tts.sh', 'stop.sh', 'play-tts.sh', 'play-tts-piper.sh', 'audio-processor.sh', 'session-start-tts.sh', 'bmad-party-speak.sh'];
|
|
5050
|
-
const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'play-tts-piper.ps1', 'audio-processor.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1'];
|
|
5208
|
+
const CRITICAL_HOOKS_WINDOWS = ['play-tts.ps1', 'play-tts-piper.ps1', 'audio-processor.ps1', 'session-start-tts.ps1', 'bmad-speak.ps1', 'bmad-party-speak.ps1', 'tts-watcher.ps1'];
|
|
5051
5209
|
|
|
5052
5210
|
/**
|
|
5053
5211
|
* Update critical hooks in the global ~/.claude/hooks/ directory if it exists.
|
|
@@ -5064,13 +5222,20 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
|
|
|
5064
5222
|
// Always ensure the global hooks dir exists so registered $HOME hooks resolve.
|
|
5065
5223
|
await fs.mkdir(globalHooksDir, { recursive: true });
|
|
5066
5224
|
|
|
5225
|
+
const manifestPath = getGlobalManifestPath(homeDir);
|
|
5226
|
+
const manifest = await loadManifest(manifestPath);
|
|
5227
|
+
const manifestUpdates = { ...manifest };
|
|
5228
|
+
|
|
5067
5229
|
for (const hook of CRITICAL_HOOKS) {
|
|
5068
5230
|
const destPath = path.join(globalHooksDir, hook);
|
|
5069
5231
|
const srcPath = path.join(srcHooksDir, hook);
|
|
5070
5232
|
try {
|
|
5071
|
-
await
|
|
5072
|
-
|
|
5073
|
-
|
|
5233
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
5234
|
+
if (result.action !== 'skipped') {
|
|
5235
|
+
if (result.action !== 'unchanged') await fs.chmod(destPath, 0o750);
|
|
5236
|
+
if (result.hash) manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
5237
|
+
updated++;
|
|
5238
|
+
}
|
|
5074
5239
|
} catch {
|
|
5075
5240
|
// src missing — skip silently
|
|
5076
5241
|
}
|
|
@@ -5084,16 +5249,74 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
|
|
|
5084
5249
|
const destPath = path.join(globalHooksWindowsDir, hook);
|
|
5085
5250
|
const srcPath = path.join(srcHooksWindowsDir, hook);
|
|
5086
5251
|
try {
|
|
5087
|
-
await
|
|
5088
|
-
|
|
5252
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
5253
|
+
if (result.action !== 'skipped') {
|
|
5254
|
+
if (result.hash) manifestUpdates[destPath] = { hash: result.hash, installedAt: new Date().toISOString() };
|
|
5255
|
+
updated++;
|
|
5256
|
+
}
|
|
5089
5257
|
} catch {
|
|
5090
5258
|
// src missing — skip silently
|
|
5091
5259
|
}
|
|
5092
5260
|
}
|
|
5093
5261
|
|
|
5262
|
+
await saveManifest(manifestPath, manifestUpdates).catch(() => { /* best effort */ });
|
|
5263
|
+
|
|
5094
5264
|
return updated;
|
|
5095
5265
|
}
|
|
5096
5266
|
|
|
5267
|
+
/**
|
|
5268
|
+
* Restart the AgentVibes TTS queue watcher on Windows after an update.
|
|
5269
|
+
* Only runs if the watcher is already installed (~/.agentvibes/tts-watcher.ps1 exists),
|
|
5270
|
+
* meaning the user previously ran setup-ssh-receiver.ps1. Silently skips for users
|
|
5271
|
+
* who don't use the SSH remote receiver.
|
|
5272
|
+
* @param {string} [homeDirOverride] - Override home dir (for testing only)
|
|
5273
|
+
* @returns {Promise<boolean>} true if watcher was restarted, false if skipped
|
|
5274
|
+
*/
|
|
5275
|
+
async function restartWatcherIfInstalled(homeDirOverride) {
|
|
5276
|
+
if (!isNativeWindows()) return false;
|
|
5277
|
+
|
|
5278
|
+
const homeDir = homeDirOverride || os.homedir();
|
|
5279
|
+
const watcherDest = path.join(homeDir, '.agentvibes', 'tts-watcher.ps1');
|
|
5280
|
+
const vbsLauncher = path.join(homeDir, '.agentvibes', 'start-watcher.vbs');
|
|
5281
|
+
|
|
5282
|
+
// Only act if the user has the SSH receiver set up
|
|
5283
|
+
try {
|
|
5284
|
+
await fs.access(watcherDest);
|
|
5285
|
+
} catch {
|
|
5286
|
+
return false;
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
const { spawnSync, spawn } = require('child_process');
|
|
5290
|
+
|
|
5291
|
+
// Kill old watcher — use array args to avoid quoting issues
|
|
5292
|
+
spawnSync('powershell.exe', [
|
|
5293
|
+
'-NoProfile', '-Command',
|
|
5294
|
+
'Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like \'*tts-watcher.ps1*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }'
|
|
5295
|
+
], { stdio: 'ignore', timeout: 8000 });
|
|
5296
|
+
|
|
5297
|
+
// Copy updated watcher from global hooks to the deployed location
|
|
5298
|
+
const watcherSrc = path.join(homeDir, '.claude', 'hooks-windows', 'tts-watcher.ps1');
|
|
5299
|
+
try {
|
|
5300
|
+
await fs.copyFile(watcherSrc, watcherDest);
|
|
5301
|
+
} catch {
|
|
5302
|
+
// src may not exist on a fresh install path — skip copy, the file was already
|
|
5303
|
+
// put there by updateGlobalHooks() earlier in the same run
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
// Restart via VBS launcher (hidden, no console flash) or fall back to direct spawn
|
|
5307
|
+
try {
|
|
5308
|
+
await fs.access(vbsLauncher);
|
|
5309
|
+
spawnSync('wscript.exe', [vbsLauncher], { stdio: 'ignore' });
|
|
5310
|
+
} catch {
|
|
5311
|
+
const ps = spawn('powershell.exe', [
|
|
5312
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', watcherDest
|
|
5313
|
+
], { detached: true, stdio: 'ignore' });
|
|
5314
|
+
ps.unref();
|
|
5315
|
+
}
|
|
5316
|
+
|
|
5317
|
+
return true;
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5097
5320
|
/**
|
|
5098
5321
|
* Perform all update operations
|
|
5099
5322
|
* @param {string} targetDir - Target installation directory
|
|
@@ -5120,6 +5343,14 @@ async function performUpdateOperations(targetDir, spinner) {
|
|
|
5120
5343
|
console.log(chalk.green(`✓ Updated ${globalHooksUpdated} critical scripts in ~/.claude/hooks/`));
|
|
5121
5344
|
}
|
|
5122
5345
|
|
|
5346
|
+
// On Windows: restart the TTS queue watcher if it was previously installed via
|
|
5347
|
+
// setup-ssh-receiver.ps1. This propagates hook updates without requiring the
|
|
5348
|
+
// user to manually run update-watcher.ps1 after every `npx agentvibes update`.
|
|
5349
|
+
const watcherRestarted = await restartWatcherIfInstalled();
|
|
5350
|
+
if (watcherRestarted) {
|
|
5351
|
+
console.log(chalk.green('✓ TTS watcher restarted with updated scripts'));
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5123
5354
|
// Update personalities
|
|
5124
5355
|
spinner.text = 'Updating personality templates...';
|
|
5125
5356
|
const srcPersonalitiesDir = path.join(__dirname, '..', '.claude', 'personalities');
|
|
@@ -5343,7 +5574,7 @@ async function install(options = {}) {
|
|
|
5343
5574
|
await installPluginManifest(targetDir, silentSpinner);
|
|
5344
5575
|
await ensureGitRepo(targetDir, silentSpinner);
|
|
5345
5576
|
|
|
5346
|
-
// Save provider configuration
|
|
5577
|
+
// Save provider configuration (always written — provider was explicitly chosen in wizard)
|
|
5347
5578
|
const providerConfigPath = path.join(claudeDir, 'tts-provider.txt');
|
|
5348
5579
|
await fs.writeFile(providerConfigPath, selectedProvider);
|
|
5349
5580
|
|
|
@@ -5457,7 +5688,8 @@ Troubleshooting:
|
|
|
5457
5688
|
default: defaultVoice = 'Samantha'; break;
|
|
5458
5689
|
}
|
|
5459
5690
|
}
|
|
5460
|
-
|
|
5691
|
+
// Only write voice on first install — preserve user's current voice selection on reinstall
|
|
5692
|
+
try { await fs.access(voiceConfigPath); } catch { await fs.writeFile(voiceConfigPath, defaultVoice); }
|
|
5461
5693
|
|
|
5462
5694
|
// Sync voice + provider to global .agentvibes/config.json so TUI finds them
|
|
5463
5695
|
// regardless of which directory it's launched from
|
|
@@ -5516,16 +5748,18 @@ Troubleshooting:
|
|
|
5516
5748
|
await fs.writeFile(translateFile, 'auto', { mode: 0o600 });
|
|
5517
5749
|
}
|
|
5518
5750
|
|
|
5519
|
-
// Apply verbosity
|
|
5520
|
-
|
|
5751
|
+
// Apply verbosity and personality — only write on first install to preserve user customisation.
|
|
5752
|
+
// To reset these, delete the files manually or use the MCP tools.
|
|
5753
|
+
const verbosityPath = path.join(claudeDir, 'tts-verbosity.txt');
|
|
5754
|
+
try { await fs.access(verbosityPath); } catch { await fs.writeFile(verbosityPath, userConfig.verbosity); }
|
|
5521
5755
|
if (userConfig.personality && userConfig.personality !== 'none') {
|
|
5522
|
-
|
|
5756
|
+
const personalityPath = path.join(claudeDir, 'tts-personality.txt');
|
|
5757
|
+
try { await fs.access(personalityPath); } catch { await fs.writeFile(personalityPath, userConfig.personality); }
|
|
5523
5758
|
}
|
|
5524
5759
|
if (userConfig.pretext && userConfig.pretext.trim()) {
|
|
5525
5760
|
await fs.writeFile(path.join(configDir, 'tts-pretext.txt'), userConfig.pretext, { mode: 0o600 });
|
|
5526
|
-
} else {
|
|
5527
|
-
try { await fs.unlink(path.join(configDir, 'tts-pretext.txt')); } catch { /* ok */ }
|
|
5528
5761
|
}
|
|
5762
|
+
// Do NOT unlink tts-pretext.txt when blank — user may have set it via MCP or manually.
|
|
5529
5763
|
|
|
5530
5764
|
// Apply reverb setting
|
|
5531
5765
|
const selectedReverb = userConfig.reverb;
|
|
@@ -5945,23 +6179,40 @@ program
|
|
|
5945
6179
|
try {
|
|
5946
6180
|
let removedCount = 0;
|
|
5947
6181
|
|
|
5948
|
-
// Remove project-level files
|
|
5949
|
-
|
|
6182
|
+
// Remove project-level files.
|
|
6183
|
+
// AgentVibes-exclusive directories are safe to rm -r.
|
|
6184
|
+
// Shared directories (.claude/hooks, .claude/personalities, etc.) are
|
|
6185
|
+
// cleaned up via the install manifest so user-added files are preserved.
|
|
6186
|
+
const exclusiveDirs = [
|
|
5950
6187
|
path.join(targetDir, '.claude', 'commands', 'agent-vibes'),
|
|
6188
|
+
path.join(targetDir, '.agentvibes'),
|
|
6189
|
+
];
|
|
6190
|
+
for (const dirPath of exclusiveDirs) {
|
|
6191
|
+
try { await fs.rm(dirPath, { recursive: true, force: true }); removedCount++; } catch { /* not present */ }
|
|
6192
|
+
}
|
|
6193
|
+
|
|
6194
|
+
// Manifest-based removal for shared directories
|
|
6195
|
+
const projectManifest = await loadManifest(getProjectManifestPath(targetDir));
|
|
6196
|
+
const sharedDirsToTryPrune = [
|
|
5951
6197
|
path.join(targetDir, '.claude', 'hooks'),
|
|
5952
6198
|
path.join(targetDir, '.claude', 'hooks-windows'),
|
|
5953
6199
|
path.join(targetDir, '.claude', 'personalities'),
|
|
5954
6200
|
path.join(targetDir, '.claude', 'output-styles'),
|
|
5955
6201
|
path.join(targetDir, '.claude', 'audio'),
|
|
5956
|
-
path.join(targetDir, '.agentvibes'),
|
|
5957
6202
|
];
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
6203
|
+
if (Object.keys(projectManifest).length > 0) {
|
|
6204
|
+
removedCount += await removeManifestFiles(projectManifest, targetDir, sharedDirsToTryPrune);
|
|
6205
|
+
} else {
|
|
6206
|
+
// No manifest (pre-manifest install) — fall back to named-file removal for safety
|
|
6207
|
+
const knownProjectFiles = [
|
|
6208
|
+
...CRITICAL_HOOKS.map(h => path.join(targetDir, '.claude', 'hooks', h)),
|
|
6209
|
+
...CRITICAL_HOOKS_WINDOWS.map(h => path.join(targetDir, '.claude', 'hooks-windows', h)),
|
|
6210
|
+
];
|
|
6211
|
+
for (const f of knownProjectFiles) {
|
|
6212
|
+
try { await fs.unlink(f); removedCount++; } catch { /* not present */ }
|
|
6213
|
+
}
|
|
6214
|
+
for (const dir of sharedDirsToTryPrune) {
|
|
6215
|
+
try { await fs.rmdir(dir); } catch { /* not empty or gone */ }
|
|
5965
6216
|
}
|
|
5966
6217
|
}
|
|
5967
6218
|
|
|
@@ -6002,33 +6253,44 @@ program
|
|
|
6002
6253
|
removedCount++;
|
|
6003
6254
|
} catch (_) { /* not present */ }
|
|
6004
6255
|
|
|
6005
|
-
// Inside ~/.claude/, remove only the
|
|
6256
|
+
// Inside ~/.claude/, remove only the files AgentVibes installed.
|
|
6257
|
+
// Use the global manifest so user-added hooks/personalities are preserved.
|
|
6258
|
+
// AgentVibes-exclusive subdir is removed via rm -r; shared dirs via manifest.
|
|
6006
6259
|
const claudeDir = path.join(homedir, '.claude');
|
|
6007
|
-
|
|
6260
|
+
try { await fs.rm(path.join(claudeDir, 'commands', 'agent-vibes'), { recursive: true, force: true }); removedCount++; } catch { /* not present */ }
|
|
6261
|
+
|
|
6262
|
+
const globalManifest = await loadManifest(getGlobalManifestPath(homedir));
|
|
6263
|
+
const globalSharedDirs = [
|
|
6008
6264
|
path.join(claudeDir, 'hooks'),
|
|
6009
6265
|
path.join(claudeDir, 'hooks-windows'),
|
|
6010
|
-
path.join(claudeDir, 'commands', 'agent-vibes'),
|
|
6011
6266
|
path.join(claudeDir, 'personalities'),
|
|
6012
6267
|
path.join(claudeDir, 'output-styles'),
|
|
6013
6268
|
path.join(claudeDir, 'audio'),
|
|
6014
6269
|
];
|
|
6270
|
+
if (Object.keys(globalManifest).length > 0) {
|
|
6271
|
+
removedCount += await removeManifestFiles(globalManifest, homedir, globalSharedDirs);
|
|
6272
|
+
} else {
|
|
6273
|
+
// No manifest — remove only known AgentVibes hook files by name
|
|
6274
|
+
const knownGlobalFiles = [
|
|
6275
|
+
...CRITICAL_HOOKS.map(h => path.join(claudeDir, 'hooks', h)),
|
|
6276
|
+
...CRITICAL_HOOKS_WINDOWS.map(h => path.join(claudeDir, 'hooks-windows', h)),
|
|
6277
|
+
];
|
|
6278
|
+
for (const f of knownGlobalFiles) {
|
|
6279
|
+
try { await fs.unlink(f); removedCount++; } catch { /* not present */ }
|
|
6280
|
+
}
|
|
6281
|
+
for (const dir of globalSharedDirs) {
|
|
6282
|
+
try { await fs.rmdir(dir); } catch { /* not empty or gone */ }
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
|
|
6015
6286
|
const agentVibesConfigFiles = [
|
|
6016
6287
|
'tts-voice.txt', 'tts-provider.txt', 'tts-personality.txt',
|
|
6017
6288
|
'tts-verbosity.txt', 'tts-translate.txt', 'tts-target-voice.txt',
|
|
6018
6289
|
'tts-target-language.txt', 'tts-language.txt', 'personalities.json',
|
|
6019
6290
|
'github-star-reminder.txt', 'piper-voices-dir.txt', 'verbosity.txt',
|
|
6020
6291
|
];
|
|
6021
|
-
|
|
6022
|
-
for (const dirPath of agentVibesOwnedInClaude) {
|
|
6023
|
-
try {
|
|
6024
|
-
await fs.rm(dirPath, { recursive: true, force: true });
|
|
6025
|
-
removedCount++;
|
|
6026
|
-
} catch (_) { /* not present */ }
|
|
6027
|
-
}
|
|
6028
6292
|
for (const fileName of agentVibesConfigFiles) {
|
|
6029
|
-
try {
|
|
6030
|
-
await fs.unlink(path.join(claudeDir, fileName));
|
|
6031
|
-
} catch (_) { /* not present */ }
|
|
6293
|
+
try { await fs.unlink(path.join(claudeDir, fileName)); } catch { /* not present */ }
|
|
6032
6294
|
}
|
|
6033
6295
|
}
|
|
6034
6296
|
|
|
@@ -6440,5 +6702,9 @@ export {
|
|
|
6440
6702
|
copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
|
|
6441
6703
|
copyConfigFiles, copyCodexFiles, configureSessionStartHook, configurePartyModeHook, ensureGitRepo,
|
|
6442
6704
|
installPluginManifest, checkAndInstallPiper,
|
|
6443
|
-
updateGlobalHooks,
|
|
6705
|
+
updateGlobalHooks, updateCommandFiles, updatePersonalityFiles,
|
|
6706
|
+
CRITICAL_HOOKS, CRITICAL_HOOKS_WINDOWS,
|
|
6707
|
+
// Manifest utilities (used by tests and external tooling)
|
|
6708
|
+
getProjectManifestPath, getGlobalManifestPath,
|
|
6709
|
+
loadManifest, saveManifest, computeFileHash, manifestSafeCopy, removeManifestFiles,
|
|
6444
6710
|
};
|