agentvibes 5.7.0 → 5.7.3
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/.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/README.md +1 -1
- package/package.json +1 -1
- package/src/console/audio-env.js +23 -0
- package/src/console/tabs/agents-tab.js +20 -47
- package/src/console/tabs/settings-tab.js +18 -44
- package/src/console/widgets/personality-picker.js +5 -2
- package/src/installer.js +260 -57
- package/src/services/llm-provider-service.js +57 -12
|
@@ -46,22 +46,25 @@ echo -e "${BLUE}🔍 Checking for BMAD files in .claude/plugins/...${NC}"
|
|
|
46
46
|
|
|
47
47
|
if [[ -f ".claude/plugins/bmad-voices-enabled.flag" ]]; then
|
|
48
48
|
echo -e "${YELLOW} Found: bmad-voices-enabled.flag${NC}"
|
|
49
|
-
mv .claude/plugins/bmad-voices-enabled.flag .agentvibes/bmad/
|
|
50
|
-
|
|
49
|
+
mv -n .claude/plugins/bmad-voices-enabled.flag .agentvibes/bmad/ && \
|
|
50
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/bmad/${NC}" || \
|
|
51
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/bmad/ — skipped${NC}"
|
|
51
52
|
MIGRATED=true
|
|
52
53
|
fi
|
|
53
54
|
|
|
54
55
|
if [[ -f ".claude/plugins/bmad-party-mode-disabled.flag" ]]; then
|
|
55
56
|
echo -e "${YELLOW} Found: bmad-party-mode-disabled.flag${NC}"
|
|
56
|
-
mv .claude/plugins/bmad-party-mode-disabled.flag .agentvibes/bmad/
|
|
57
|
-
|
|
57
|
+
mv -n .claude/plugins/bmad-party-mode-disabled.flag .agentvibes/bmad/ && \
|
|
58
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/bmad/${NC}" || \
|
|
59
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/bmad/ — skipped${NC}"
|
|
58
60
|
MIGRATED=true
|
|
59
61
|
fi
|
|
60
62
|
|
|
61
63
|
if [[ -f ".claude/plugins/.bmad-previous-settings" ]]; then
|
|
62
64
|
echo -e "${YELLOW} Found: .bmad-previous-settings${NC}"
|
|
63
|
-
mv .claude/plugins/.bmad-previous-settings .agentvibes/bmad/
|
|
64
|
-
|
|
65
|
+
mv -n .claude/plugins/.bmad-previous-settings .agentvibes/bmad/ && \
|
|
66
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/bmad/${NC}" || \
|
|
67
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/bmad/ — skipped${NC}"
|
|
65
68
|
MIGRATED=true
|
|
66
69
|
fi
|
|
67
70
|
|
|
@@ -72,8 +75,9 @@ echo -e "${BLUE}🔍 Checking for BMAD files in .claude/config/...${NC}"
|
|
|
72
75
|
|
|
73
76
|
if [[ -f ".claude/config/bmad-voices.md" ]]; then
|
|
74
77
|
echo -e "${YELLOW} Found: bmad-voices.md${NC}"
|
|
75
|
-
mv .claude/config/bmad-voices.md .agentvibes/bmad/
|
|
76
|
-
|
|
78
|
+
mv -n .claude/config/bmad-voices.md .agentvibes/bmad/ && \
|
|
79
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/bmad/${NC}" || \
|
|
80
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/bmad/ — skipped${NC}"
|
|
77
81
|
MIGRATED=true
|
|
78
82
|
fi
|
|
79
83
|
|
|
@@ -97,29 +101,33 @@ echo -e "${BLUE}🔍 Checking for AgentVibes config in .claude/config/...${NC}"
|
|
|
97
101
|
|
|
98
102
|
if [[ -f ".claude/config/agentvibes.json" ]]; then
|
|
99
103
|
echo -e "${YELLOW} Found: agentvibes.json${NC}"
|
|
100
|
-
mv .claude/config/agentvibes.json .agentvibes/config/
|
|
101
|
-
|
|
104
|
+
mv -n .claude/config/agentvibes.json .agentvibes/config/ && \
|
|
105
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/config/${NC}" || \
|
|
106
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/config/ — skipped${NC}"
|
|
102
107
|
MIGRATED=true
|
|
103
108
|
fi
|
|
104
109
|
|
|
105
110
|
if [[ -f ".claude/config/personality-voice-defaults.default.json" ]]; then
|
|
106
111
|
echo -e "${YELLOW} Found: personality-voice-defaults.default.json${NC}"
|
|
107
|
-
mv .claude/config/personality-voice-defaults.default.json .agentvibes/config/
|
|
108
|
-
|
|
112
|
+
mv -n .claude/config/personality-voice-defaults.default.json .agentvibes/config/ && \
|
|
113
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/config/${NC}" || \
|
|
114
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/config/ — skipped${NC}"
|
|
109
115
|
MIGRATED=true
|
|
110
116
|
fi
|
|
111
117
|
|
|
112
118
|
if [[ -f ".claude/config/personality-voice-defaults.json" ]]; then
|
|
113
119
|
echo -e "${YELLOW} Found: personality-voice-defaults.json${NC}"
|
|
114
|
-
mv .claude/config/personality-voice-defaults.json .agentvibes/config/
|
|
115
|
-
|
|
120
|
+
mv -n .claude/config/personality-voice-defaults.json .agentvibes/config/ && \
|
|
121
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/config/${NC}" || \
|
|
122
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/config/ — skipped${NC}"
|
|
116
123
|
MIGRATED=true
|
|
117
124
|
fi
|
|
118
125
|
|
|
119
126
|
if [[ -f ".claude/config/README-personality-defaults.md" ]]; then
|
|
120
127
|
echo -e "${YELLOW} Found: README-personality-defaults.md${NC}"
|
|
121
|
-
mv .claude/config/README-personality-defaults.md .agentvibes/config/
|
|
122
|
-
|
|
128
|
+
mv -n .claude/config/README-personality-defaults.md .agentvibes/config/ && \
|
|
129
|
+
echo -e "${GREEN} ✓ Moved to .agentvibes/config/${NC}" || \
|
|
130
|
+
echo -e "${YELLOW} ⚠ Already exists in .agentvibes/config/ — skipped${NC}"
|
|
123
131
|
MIGRATED=true
|
|
124
132
|
fi
|
|
125
133
|
|
|
@@ -276,6 +276,13 @@ case "$1" in
|
|
|
276
276
|
exit 1
|
|
277
277
|
fi
|
|
278
278
|
|
|
279
|
+
# Validate name: alphanumeric, hyphens, underscores only — no path traversal
|
|
280
|
+
if [[ ! "$NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
281
|
+
echo "❌ Invalid personality name '$NAME'"
|
|
282
|
+
echo " Name must contain only letters, numbers, hyphens, and underscores"
|
|
283
|
+
exit 1
|
|
284
|
+
fi
|
|
285
|
+
|
|
279
286
|
FILE="$PERSONALITIES_DIR/${NAME}.md"
|
|
280
287
|
if [[ -f "$FILE" ]]; then
|
|
281
288
|
echo "❌ Personality '$NAME' already exists"
|
|
@@ -306,8 +313,8 @@ Describe how the AI should generate messages for this personality.
|
|
|
306
313
|
- "Example response 2"
|
|
307
314
|
EOF
|
|
308
315
|
|
|
309
|
-
# Replace NAME with
|
|
310
|
-
sed -i "s
|
|
316
|
+
# Replace NAME placeholder — use | as delimiter to avoid issues with / in paths
|
|
317
|
+
sed -i "s|NAME|${NAME}|g" "$FILE"
|
|
311
318
|
|
|
312
319
|
echo "✅ Created new personality: $NAME"
|
|
313
320
|
echo "📝 Edit the file: $FILE"
|
|
@@ -327,6 +334,12 @@ EOF
|
|
|
327
334
|
exit 1
|
|
328
335
|
fi
|
|
329
336
|
|
|
337
|
+
if [[ ! "$NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
338
|
+
echo "❌ Invalid personality name '$NAME'"
|
|
339
|
+
echo " Name must contain only letters, numbers, hyphens, and underscores"
|
|
340
|
+
exit 1
|
|
341
|
+
fi
|
|
342
|
+
|
|
330
343
|
FILE="$PERSONALITIES_DIR/${NAME}.md"
|
|
331
344
|
if [[ ! -f "$FILE" ]]; then
|
|
332
345
|
echo "❌ Personality '$NAME' not found"
|
|
@@ -335,6 +335,12 @@ if [[ -n "$VOICE_OVERRIDE" ]]; then
|
|
|
335
335
|
esac
|
|
336
336
|
fi
|
|
337
337
|
|
|
338
|
+
# Emit resolved voice and provider in verbose mode (used by tests and diagnostics)
|
|
339
|
+
if [[ "${AGENTVIBES_VERBOSE:-0}" == "1" ]]; then
|
|
340
|
+
[[ -n "${VOICE_OVERRIDE:-}" ]] && echo "voice=${VOICE_OVERRIDE}" >&2
|
|
341
|
+
echo "provider=${ACTIVE_PROVIDER}" >&2
|
|
342
|
+
fi
|
|
343
|
+
|
|
338
344
|
# @function speak_text
|
|
339
345
|
# @intent Route text to appropriate TTS provider
|
|
340
346
|
# @why Reusable function for speaking, used by both single and learning modes
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](https://github.com/paulpreibisch/AgentVibes/actions/workflows/publish.yml)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
|
|
14
|
-
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.
|
|
14
|
+
**Author**: Paul Preibisch ([@997Fire](https://x.com/997Fire)) | **Version**: v5.7.3
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "agentvibes",
|
|
4
|
-
"version": "5.7.
|
|
4
|
+
"version": "5.7.3",
|
|
5
5
|
"description": "Now your AI Agents can finally talk back! Professional TTS voice for Claude Code, Claude Desktop (via MCP), and Clawdbot with multi-provider support.",
|
|
6
6
|
"homepage": "https://agentvibes.org",
|
|
7
7
|
"keywords": [
|
package/src/console/audio-env.js
CHANGED
|
@@ -109,6 +109,29 @@ function _detect(players, env) {
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Detect the first LLM configured with remote transport in
|
|
114
|
+
* ~/.agentvibes/transport-config.json.
|
|
115
|
+
*
|
|
116
|
+
* Returns the LLM key (e.g. "claude-code") whose `mode` is "remote", or
|
|
117
|
+
* null if no such entry exists or the file is absent/invalid.
|
|
118
|
+
* Used by TUI voice-preview functions to route audio through the correct
|
|
119
|
+
* SSH transport rather than playing locally.
|
|
120
|
+
*
|
|
121
|
+
* @returns {string|null}
|
|
122
|
+
*/
|
|
123
|
+
export function detectRemoteLlm() {
|
|
124
|
+
const cfgPath = path.join(os.homedir(), '.agentvibes', 'transport-config.json');
|
|
125
|
+
if (!fs.existsSync(cfgPath)) return null;
|
|
126
|
+
try {
|
|
127
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
128
|
+
for (const [llm, opts] of Object.entries(cfg)) {
|
|
129
|
+
if (opts && opts.mode === 'remote') return llm;
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
112
135
|
/**
|
|
113
136
|
* Detect the best available MP3 player.
|
|
114
137
|
* On Windows, falls back to ffplay/mpv if installed, otherwise null.
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
PIPER_VOICES_DIR, SAMPLE_PHRASES,
|
|
19
19
|
parseMultiSpeaker, scanInstalledVoices, getVoiceMeta, genderIconTag,
|
|
20
20
|
} from './voices-tab.js';
|
|
21
|
-
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
21
|
+
import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
|
|
22
22
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
23
23
|
import { BRAND_PINK } from '../brand-colors.js';
|
|
24
24
|
import { t } from '../../i18n/strings.js';
|
|
@@ -1031,64 +1031,35 @@ ${_tl('bmadDesc')}
|
|
|
1031
1031
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); screen.render(); return; }
|
|
1032
1032
|
_killVP();
|
|
1033
1033
|
|
|
1034
|
-
const _isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
|
|
1035
|
-
|
|
1036
|
-
const _ms = parseMultiSpeaker(voiceId);
|
|
1037
|
-
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
1038
|
-
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
1039
|
-
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
1040
|
-
|
|
1041
|
-
const tempWav = _secureTempWav('vp');
|
|
1042
1034
|
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
1035
|
+
const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
1036
|
+
if (!fs.existsSync(playTtsScript)) return;
|
|
1043
1037
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (
|
|
1047
|
-
const _lad = process.env.LOCALAPPDATA ||
|
|
1048
|
-
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
1049
|
-
if (_lad) {
|
|
1050
|
-
const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
|
|
1051
|
-
if (fs.existsSync(_ep)) _piperBin = _ep;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1038
|
+
const remoteLlm = detectRemoteLlm();
|
|
1039
|
+
const args = [playTtsScript, phrase, voiceId];
|
|
1040
|
+
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
1054
1041
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1059
|
-
detached: !_isWin,
|
|
1060
|
-
windowsHide: true,
|
|
1042
|
+
_previewProc = spawn('bash', args, {
|
|
1043
|
+
stdio: 'ignore',
|
|
1044
|
+
detached: true,
|
|
1061
1045
|
env: _spawnEnv,
|
|
1046
|
+
cwd: _projectRoot,
|
|
1062
1047
|
});
|
|
1063
|
-
piper.stdin.write(phrase + '\n');
|
|
1064
|
-
piper.stdin.end();
|
|
1065
|
-
_previewProc = piper;
|
|
1066
1048
|
_previewVoiceId = voiceId;
|
|
1067
1049
|
|
|
1068
1050
|
if (!_vpClosed) {
|
|
1069
|
-
vpPreviewLine.setContent(`{bright-cyan-fg}♪
|
|
1051
|
+
vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}...{/bright-cyan-fg}`);
|
|
1070
1052
|
screen.render();
|
|
1071
1053
|
}
|
|
1072
1054
|
|
|
1073
|
-
|
|
1074
|
-
if (_previewVoiceId
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
stdio: 'ignore',
|
|
1080
|
-
detached: !_isWin,
|
|
1081
|
-
windowsHide: true,
|
|
1082
|
-
env: _spawnEnv,
|
|
1083
|
-
});
|
|
1084
|
-
_previewProc = pp;
|
|
1085
|
-
if (!_vpClosed) { vpPreviewLine.setContent(`{bright-cyan-fg}♪ Playing: ${voiceId}{/bright-cyan-fg}`); screen.render(); }
|
|
1086
|
-
pp.on('exit', () => {
|
|
1087
|
-
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); } }
|
|
1088
|
-
try { fs.unlinkSync(tempWav); } catch {}
|
|
1089
|
-
});
|
|
1055
|
+
_previewProc.on('exit', () => {
|
|
1056
|
+
if (_previewVoiceId === voiceId) {
|
|
1057
|
+
_previewVoiceId = null;
|
|
1058
|
+
_previewProc = null;
|
|
1059
|
+
if (!_vpClosed) { vpPreviewLine.setContent(''); screen.render(); }
|
|
1060
|
+
}
|
|
1090
1061
|
});
|
|
1091
|
-
|
|
1062
|
+
_previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
1092
1063
|
}
|
|
1093
1064
|
|
|
1094
1065
|
vpList.key(['enter'], () => {
|
|
@@ -1220,8 +1191,10 @@ ${_tl('bmadDesc')}
|
|
|
1220
1191
|
const _spawnEnv = buildAudioEnv();
|
|
1221
1192
|
const scriptDir = path.join(_projectRoot, '.claude', 'hooks');
|
|
1222
1193
|
const plainScript = path.join(scriptDir, 'play-tts.sh');
|
|
1194
|
+
const remoteLlm = detectRemoteLlm();
|
|
1223
1195
|
const args = [plainScript, phrase];
|
|
1224
1196
|
if (voiceId) args.push(voiceId);
|
|
1197
|
+
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
1225
1198
|
|
|
1226
1199
|
const proc = spawn('bash', args, {
|
|
1227
1200
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from './voices-tab.js';
|
|
25
25
|
import { LanguageService } from '../../services/language-service.js';
|
|
26
26
|
import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
|
|
27
|
-
import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
|
|
27
|
+
import { buildAudioEnv, detectWavPlayer, detectRemoteLlm } from '../audio-env.js';
|
|
28
28
|
import { destroyList } from '../widgets/destroy-list.js';
|
|
29
29
|
import { openReverbPicker } from '../widgets/reverb-picker.js';
|
|
30
30
|
import { openPersonalityPicker } from '../widgets/personality-picker.js';
|
|
@@ -633,61 +633,35 @@ export function createSettingsTab(screen, services) {
|
|
|
633
633
|
if (_previewVoiceId === voiceId) { _killVP(); vpPreviewLine.setContent(''); _refreshVP(); return; }
|
|
634
634
|
_killVP();
|
|
635
635
|
|
|
636
|
-
const _ms = parseMultiSpeaker(voiceId);
|
|
637
|
-
const voicePath = path.resolve(PIPER_VOICES_DIR, _ms.model + '.onnx');
|
|
638
|
-
const safeBase = path.resolve(PIPER_VOICES_DIR);
|
|
639
|
-
if (!voicePath.startsWith(safeBase + path.sep) && voicePath !== safeBase) return;
|
|
640
|
-
|
|
641
|
-
const tempWav = _secureTempWav('vp');
|
|
642
636
|
const phrase = SAMPLE_PHRASES[Math.floor(Math.random() * SAMPLE_PHRASES.length)];
|
|
637
|
+
const playTtsScript = path.join(_projectRoot, '.claude', 'hooks', 'play-tts.sh');
|
|
638
|
+
if (!fs.existsSync(playTtsScript)) return;
|
|
643
639
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
(process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
|
648
|
-
if (_lad) {
|
|
649
|
-
const _ep = path.join(_lad, 'Programs', 'Piper', 'piper.exe');
|
|
650
|
-
if (fs.existsSync(_ep)) _piperBin = _ep;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
640
|
+
const remoteLlm = detectRemoteLlm();
|
|
641
|
+
const args = [playTtsScript, phrase, voiceId];
|
|
642
|
+
if (remoteLlm) args.push('--llm', remoteLlm);
|
|
653
643
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
658
|
-
detached: !_isWin,
|
|
659
|
-
windowsHide: true,
|
|
644
|
+
_previewProc = spawn('bash', args, {
|
|
645
|
+
stdio: 'ignore',
|
|
646
|
+
detached: true,
|
|
660
647
|
env: _spawnEnv,
|
|
648
|
+
cwd: _projectRoot,
|
|
661
649
|
});
|
|
662
|
-
piper.stdin.write(phrase + '\n');
|
|
663
|
-
piper.stdin.end();
|
|
664
|
-
_previewProc = piper;
|
|
665
650
|
_previewVoiceId = voiceId;
|
|
666
651
|
|
|
667
652
|
if (!_vpClosed) {
|
|
668
|
-
vpPreviewLine.setContent(`{cyan-fg}♪
|
|
653
|
+
vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}...{/cyan-fg}`);
|
|
669
654
|
_refreshVP();
|
|
670
655
|
}
|
|
671
656
|
|
|
672
|
-
|
|
673
|
-
if (_previewVoiceId
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
stdio: 'ignore',
|
|
679
|
-
detached: !_isWin,
|
|
680
|
-
windowsHide: true,
|
|
681
|
-
env: _spawnEnv,
|
|
682
|
-
});
|
|
683
|
-
_previewProc = pp;
|
|
684
|
-
if (!_vpClosed) { vpPreviewLine.setContent(`{cyan-fg}♪ Playing: ${voiceId}{/cyan-fg}`); screen.render(); }
|
|
685
|
-
pp.on('exit', () => {
|
|
686
|
-
if (_previewVoiceId === voiceId) { _previewVoiceId = null; _previewProc = null; if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); } }
|
|
687
|
-
try { fs.unlinkSync(tempWav); } catch {}
|
|
688
|
-
});
|
|
657
|
+
_previewProc.on('exit', () => {
|
|
658
|
+
if (_previewVoiceId === voiceId) {
|
|
659
|
+
_previewVoiceId = null;
|
|
660
|
+
_previewProc = null;
|
|
661
|
+
if (!_vpClosed) { vpPreviewLine.setContent(''); _refreshVP(); }
|
|
662
|
+
}
|
|
689
663
|
});
|
|
690
|
-
|
|
664
|
+
_previewProc.on('error', () => { _previewProc = null; _previewVoiceId = null; });
|
|
691
665
|
}
|
|
692
666
|
|
|
693
667
|
vpList.key(['enter'], () => {
|
|
@@ -10,7 +10,7 @@ import fs from 'node:fs';
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { destroyList } from './destroy-list.js';
|
|
13
|
-
import { buildAudioEnv } from '../audio-env.js';
|
|
13
|
+
import { buildAudioEnv, detectRemoteLlm } from '../audio-env.js';
|
|
14
14
|
import { BRAND_PINK } from '../brand-colors.js';
|
|
15
15
|
import { PERSONALITY_EMOJIS, PERSONALITIES } from '../constants/personalities.js';
|
|
16
16
|
|
|
@@ -138,7 +138,10 @@ export function openPersonalityPicker(screen, currentPersonality, onSelect, onCl
|
|
|
138
138
|
});
|
|
139
139
|
} else {
|
|
140
140
|
const ttsScript = path.join(process.cwd(), '.claude', 'hooks', 'play-tts.sh');
|
|
141
|
-
|
|
141
|
+
const remoteLlm = detectRemoteLlm();
|
|
142
|
+
const ttsArgs = [ttsScript, phrase];
|
|
143
|
+
if (remoteLlm) ttsArgs.push('--llm', remoteLlm);
|
|
144
|
+
_pickerTtsProc = spawn('bash', ttsArgs, {
|
|
142
145
|
stdio: 'ignore',
|
|
143
146
|
detached: true,
|
|
144
147
|
env: _env,
|
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'));
|
|
@@ -4982,6 +5116,10 @@ async function updatePersonalityFiles(targetDir, srcPersonalitiesDir) {
|
|
|
4982
5116
|
let newPersonalities = 0;
|
|
4983
5117
|
let updatedPersonalities = 0;
|
|
4984
5118
|
|
|
5119
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
5120
|
+
const manifest = await loadManifest(manifestPath);
|
|
5121
|
+
const manifestUpdates = {};
|
|
5122
|
+
|
|
4985
5123
|
for (const file of allPersonalityFiles) {
|
|
4986
5124
|
const srcPath = path.join(srcPersonalitiesDir, file);
|
|
4987
5125
|
const stat = await fs.stat(srcPath);
|
|
@@ -4991,17 +5129,24 @@ async function updatePersonalityFiles(targetDir, srcPersonalitiesDir) {
|
|
|
4991
5129
|
}
|
|
4992
5130
|
|
|
4993
5131
|
const destPath = path.join(destPersonalitiesDir, file);
|
|
5132
|
+
const result = await manifestSafeCopy(srcPath, destPath, manifest);
|
|
4994
5133
|
|
|
4995
|
-
|
|
4996
|
-
await fs.access(destPath);
|
|
4997
|
-
await fs.copyFile(srcPath, destPath);
|
|
4998
|
-
updatedPersonalities++;
|
|
4999
|
-
} catch {
|
|
5000
|
-
await fs.copyFile(srcPath, destPath);
|
|
5134
|
+
if (result.action === 'new') {
|
|
5001
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() };
|
|
5002
5143
|
}
|
|
5003
5144
|
}
|
|
5004
5145
|
|
|
5146
|
+
if (Object.keys(manifestUpdates).length > 0) {
|
|
5147
|
+
await saveManifest(manifestPath, { ...manifest, ...manifestUpdates }).catch(() => { /* best effort */ });
|
|
5148
|
+
}
|
|
5149
|
+
|
|
5005
5150
|
return { new: newPersonalities, updated: updatedPersonalities };
|
|
5006
5151
|
}
|
|
5007
5152
|
|
|
@@ -5033,10 +5178,21 @@ async function updateCommandFiles(targetDir, spinner) {
|
|
|
5033
5178
|
const srcCommandsDir = path.join(__dirname, '..', '.claude', 'commands', 'agent-vibes');
|
|
5034
5179
|
const commandFiles = await fs.readdir(srcCommandsDir);
|
|
5035
5180
|
|
|
5181
|
+
const manifestPath = getProjectManifestPath(targetDir);
|
|
5182
|
+
const manifest = await loadManifest(manifestPath);
|
|
5183
|
+
const manifestUpdates = {};
|
|
5184
|
+
|
|
5036
5185
|
for (const file of commandFiles) {
|
|
5037
5186
|
const srcPath = path.join(srcCommandsDir, file);
|
|
5038
5187
|
const destPath = path.join(commandsDir, file);
|
|
5039
|
-
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 */ });
|
|
5040
5196
|
}
|
|
5041
5197
|
|
|
5042
5198
|
return commandFiles.length;
|
|
@@ -5066,13 +5222,20 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
|
|
|
5066
5222
|
// Always ensure the global hooks dir exists so registered $HOME hooks resolve.
|
|
5067
5223
|
await fs.mkdir(globalHooksDir, { recursive: true });
|
|
5068
5224
|
|
|
5225
|
+
const manifestPath = getGlobalManifestPath(homeDir);
|
|
5226
|
+
const manifest = await loadManifest(manifestPath);
|
|
5227
|
+
const manifestUpdates = { ...manifest };
|
|
5228
|
+
|
|
5069
5229
|
for (const hook of CRITICAL_HOOKS) {
|
|
5070
5230
|
const destPath = path.join(globalHooksDir, hook);
|
|
5071
5231
|
const srcPath = path.join(srcHooksDir, hook);
|
|
5072
5232
|
try {
|
|
5073
|
-
await
|
|
5074
|
-
|
|
5075
|
-
|
|
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
|
+
}
|
|
5076
5239
|
} catch {
|
|
5077
5240
|
// src missing — skip silently
|
|
5078
5241
|
}
|
|
@@ -5086,13 +5249,18 @@ async function updateGlobalHooks(srcHooksDir, homeDirOverride) {
|
|
|
5086
5249
|
const destPath = path.join(globalHooksWindowsDir, hook);
|
|
5087
5250
|
const srcPath = path.join(srcHooksWindowsDir, hook);
|
|
5088
5251
|
try {
|
|
5089
|
-
await
|
|
5090
|
-
|
|
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
|
+
}
|
|
5091
5257
|
} catch {
|
|
5092
5258
|
// src missing — skip silently
|
|
5093
5259
|
}
|
|
5094
5260
|
}
|
|
5095
5261
|
|
|
5262
|
+
await saveManifest(manifestPath, manifestUpdates).catch(() => { /* best effort */ });
|
|
5263
|
+
|
|
5096
5264
|
return updated;
|
|
5097
5265
|
}
|
|
5098
5266
|
|
|
@@ -5406,7 +5574,7 @@ async function install(options = {}) {
|
|
|
5406
5574
|
await installPluginManifest(targetDir, silentSpinner);
|
|
5407
5575
|
await ensureGitRepo(targetDir, silentSpinner);
|
|
5408
5576
|
|
|
5409
|
-
// Save provider configuration
|
|
5577
|
+
// Save provider configuration (always written — provider was explicitly chosen in wizard)
|
|
5410
5578
|
const providerConfigPath = path.join(claudeDir, 'tts-provider.txt');
|
|
5411
5579
|
await fs.writeFile(providerConfigPath, selectedProvider);
|
|
5412
5580
|
|
|
@@ -5520,7 +5688,8 @@ Troubleshooting:
|
|
|
5520
5688
|
default: defaultVoice = 'Samantha'; break;
|
|
5521
5689
|
}
|
|
5522
5690
|
}
|
|
5523
|
-
|
|
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); }
|
|
5524
5693
|
|
|
5525
5694
|
// Sync voice + provider to global .agentvibes/config.json so TUI finds them
|
|
5526
5695
|
// regardless of which directory it's launched from
|
|
@@ -5579,16 +5748,18 @@ Troubleshooting:
|
|
|
5579
5748
|
await fs.writeFile(translateFile, 'auto', { mode: 0o600 });
|
|
5580
5749
|
}
|
|
5581
5750
|
|
|
5582
|
-
// Apply verbosity
|
|
5583
|
-
|
|
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); }
|
|
5584
5755
|
if (userConfig.personality && userConfig.personality !== 'none') {
|
|
5585
|
-
|
|
5756
|
+
const personalityPath = path.join(claudeDir, 'tts-personality.txt');
|
|
5757
|
+
try { await fs.access(personalityPath); } catch { await fs.writeFile(personalityPath, userConfig.personality); }
|
|
5586
5758
|
}
|
|
5587
5759
|
if (userConfig.pretext && userConfig.pretext.trim()) {
|
|
5588
5760
|
await fs.writeFile(path.join(configDir, 'tts-pretext.txt'), userConfig.pretext, { mode: 0o600 });
|
|
5589
|
-
} else {
|
|
5590
|
-
try { await fs.unlink(path.join(configDir, 'tts-pretext.txt')); } catch { /* ok */ }
|
|
5591
5761
|
}
|
|
5762
|
+
// Do NOT unlink tts-pretext.txt when blank — user may have set it via MCP or manually.
|
|
5592
5763
|
|
|
5593
5764
|
// Apply reverb setting
|
|
5594
5765
|
const selectedReverb = userConfig.reverb;
|
|
@@ -6008,23 +6179,40 @@ program
|
|
|
6008
6179
|
try {
|
|
6009
6180
|
let removedCount = 0;
|
|
6010
6181
|
|
|
6011
|
-
// Remove project-level files
|
|
6012
|
-
|
|
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 = [
|
|
6013
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 = [
|
|
6014
6197
|
path.join(targetDir, '.claude', 'hooks'),
|
|
6015
6198
|
path.join(targetDir, '.claude', 'hooks-windows'),
|
|
6016
6199
|
path.join(targetDir, '.claude', 'personalities'),
|
|
6017
6200
|
path.join(targetDir, '.claude', 'output-styles'),
|
|
6018
6201
|
path.join(targetDir, '.claude', 'audio'),
|
|
6019
|
-
path.join(targetDir, '.agentvibes'),
|
|
6020
6202
|
];
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
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 */ }
|
|
6028
6216
|
}
|
|
6029
6217
|
}
|
|
6030
6218
|
|
|
@@ -6065,33 +6253,44 @@ program
|
|
|
6065
6253
|
removedCount++;
|
|
6066
6254
|
} catch (_) { /* not present */ }
|
|
6067
6255
|
|
|
6068
|
-
// 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.
|
|
6069
6259
|
const claudeDir = path.join(homedir, '.claude');
|
|
6070
|
-
|
|
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 = [
|
|
6071
6264
|
path.join(claudeDir, 'hooks'),
|
|
6072
6265
|
path.join(claudeDir, 'hooks-windows'),
|
|
6073
|
-
path.join(claudeDir, 'commands', 'agent-vibes'),
|
|
6074
6266
|
path.join(claudeDir, 'personalities'),
|
|
6075
6267
|
path.join(claudeDir, 'output-styles'),
|
|
6076
6268
|
path.join(claudeDir, 'audio'),
|
|
6077
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
|
+
|
|
6078
6286
|
const agentVibesConfigFiles = [
|
|
6079
6287
|
'tts-voice.txt', 'tts-provider.txt', 'tts-personality.txt',
|
|
6080
6288
|
'tts-verbosity.txt', 'tts-translate.txt', 'tts-target-voice.txt',
|
|
6081
6289
|
'tts-target-language.txt', 'tts-language.txt', 'personalities.json',
|
|
6082
6290
|
'github-star-reminder.txt', 'piper-voices-dir.txt', 'verbosity.txt',
|
|
6083
6291
|
];
|
|
6084
|
-
|
|
6085
|
-
for (const dirPath of agentVibesOwnedInClaude) {
|
|
6086
|
-
try {
|
|
6087
|
-
await fs.rm(dirPath, { recursive: true, force: true });
|
|
6088
|
-
removedCount++;
|
|
6089
|
-
} catch (_) { /* not present */ }
|
|
6090
|
-
}
|
|
6091
6292
|
for (const fileName of agentVibesConfigFiles) {
|
|
6092
|
-
try {
|
|
6093
|
-
await fs.unlink(path.join(claudeDir, fileName));
|
|
6094
|
-
} catch (_) { /* not present */ }
|
|
6293
|
+
try { await fs.unlink(path.join(claudeDir, fileName)); } catch { /* not present */ }
|
|
6095
6294
|
}
|
|
6096
6295
|
}
|
|
6097
6296
|
|
|
@@ -6503,5 +6702,9 @@ export {
|
|
|
6503
6702
|
copyPluginFiles, copyBmadConfigFiles, copyBackgroundMusicFiles,
|
|
6504
6703
|
copyConfigFiles, copyCodexFiles, configureSessionStartHook, configurePartyModeHook, ensureGitRepo,
|
|
6505
6704
|
installPluginManifest, checkAndInstallPiper,
|
|
6506
|
-
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,
|
|
6507
6710
|
};
|
|
@@ -481,19 +481,54 @@ export async function removeCopilotMcp(targetDir) {
|
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
const AGENTVIBES_MARKER_START = '<!-- BEGIN AGENTVIBES -->';
|
|
485
|
+
const AGENTVIBES_MARKER_END = '<!-- END AGENTVIBES -->';
|
|
486
|
+
|
|
487
|
+
function wrapWithMarkers(content) {
|
|
488
|
+
return `${AGENTVIBES_MARKER_START}\n${content.trim()}\n${AGENTVIBES_MARKER_END}\n`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function injectMarkerBlock(existing, newBlock) {
|
|
492
|
+
const start = existing.indexOf(AGENTVIBES_MARKER_START);
|
|
493
|
+
const end = existing.indexOf(AGENTVIBES_MARKER_END);
|
|
494
|
+
if (start !== -1 && end !== -1) {
|
|
495
|
+
return existing.substring(0, start) + newBlock + existing.substring(end + AGENTVIBES_MARKER_END.length + 1);
|
|
496
|
+
}
|
|
497
|
+
return existing.trimEnd() + (existing.trim() ? '\n\n' : '') + newBlock;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function removeMarkerBlock(existing) {
|
|
501
|
+
const start = existing.indexOf(AGENTVIBES_MARKER_START);
|
|
502
|
+
const end = existing.indexOf(AGENTVIBES_MARKER_END);
|
|
503
|
+
if (start === -1 || end === -1) return existing;
|
|
504
|
+
const before = existing.substring(0, start).trimEnd();
|
|
505
|
+
const after = existing.substring(end + AGENTVIBES_MARKER_END.length).replace(/^\n+/, '\n');
|
|
506
|
+
return (before + after).trimEnd() + '\n';
|
|
507
|
+
}
|
|
508
|
+
|
|
484
509
|
export async function installCopilotInstructions(targetDir, packageDir) {
|
|
485
510
|
const destPath = path.join(targetDir, '.github', 'copilot-instructions.md');
|
|
486
511
|
const srcPath = path.join(packageDir, '.github', 'copilot-instructions.md');
|
|
487
512
|
try {
|
|
488
513
|
await fs.mkdir(path.join(targetDir, '.github'), { recursive: true });
|
|
489
|
-
const
|
|
490
|
-
|
|
514
|
+
const srcContent = await fs.readFile(srcPath, 'utf8');
|
|
515
|
+
let existing = '';
|
|
516
|
+
try { existing = await fs.readFile(destPath, 'utf8'); } catch { /* new file */ }
|
|
517
|
+
const block = wrapWithMarkers(srcContent);
|
|
518
|
+
await fs.writeFile(destPath, injectMarkerBlock(existing, block));
|
|
491
519
|
} catch { /* best effort */ }
|
|
492
520
|
}
|
|
493
521
|
|
|
494
522
|
export async function removeCopilotInstructions(targetDir) {
|
|
523
|
+
const destPath = path.join(targetDir, '.github', 'copilot-instructions.md');
|
|
495
524
|
try {
|
|
496
|
-
await fs.
|
|
525
|
+
const existing = await fs.readFile(destPath, 'utf8');
|
|
526
|
+
const updated = removeMarkerBlock(existing);
|
|
527
|
+
if (updated.trim()) {
|
|
528
|
+
await fs.writeFile(destPath, updated);
|
|
529
|
+
} else {
|
|
530
|
+
await fs.unlink(destPath);
|
|
531
|
+
}
|
|
497
532
|
} catch { /* already gone */ }
|
|
498
533
|
}
|
|
499
534
|
|
|
@@ -582,10 +617,15 @@ export function buildCodexToml(existingContent = '') {
|
|
|
582
617
|
export async function installCodexInstructions(targetDir, packageDir) {
|
|
583
618
|
const srcPath = path.join(packageDir, '.codex', 'AGENTS.md');
|
|
584
619
|
try {
|
|
585
|
-
const
|
|
620
|
+
const srcContent = await fs.readFile(srcPath, 'utf8');
|
|
621
|
+
const block = wrapWithMarkers(srcContent);
|
|
586
622
|
await fs.mkdir(path.join(targetDir, '.codex'), { recursive: true });
|
|
587
|
-
|
|
588
|
-
|
|
623
|
+
|
|
624
|
+
for (const dest of [path.join(targetDir, '.codex', 'AGENTS.md'), path.join(targetDir, 'AGENTS.md')]) {
|
|
625
|
+
let existing = '';
|
|
626
|
+
try { existing = await fs.readFile(dest, 'utf8'); } catch { /* new file */ }
|
|
627
|
+
await fs.writeFile(dest, injectMarkerBlock(existing, block));
|
|
628
|
+
}
|
|
589
629
|
} catch { /* best effort */ }
|
|
590
630
|
}
|
|
591
631
|
|
|
@@ -604,12 +644,17 @@ export async function installCodexHooks(targetDir, packageDir) {
|
|
|
604
644
|
}
|
|
605
645
|
|
|
606
646
|
export async function removeCodexInstructions(targetDir) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
647
|
+
for (const dest of [path.join(targetDir, '.codex', 'AGENTS.md'), path.join(targetDir, 'AGENTS.md')]) {
|
|
648
|
+
try {
|
|
649
|
+
const existing = await fs.readFile(dest, 'utf8');
|
|
650
|
+
const updated = removeMarkerBlock(existing);
|
|
651
|
+
if (updated.trim()) {
|
|
652
|
+
await fs.writeFile(dest, updated);
|
|
653
|
+
} else {
|
|
654
|
+
await fs.unlink(dest);
|
|
655
|
+
}
|
|
656
|
+
} catch { /* already gone */ }
|
|
657
|
+
}
|
|
613
658
|
}
|
|
614
659
|
|
|
615
660
|
export async function removeCodexHooks(targetDir) {
|