agentvibes 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/config/audio-effects.cfg +3 -2
- package/.claude/config/background-music-position.txt +1 -1
- package/.claude/hooks/audio-processor.sh +87 -43
- package/.claude/hooks/bmad-speak.sh +184 -27
- package/.claude/hooks/play-tts-enhanced.sh +40 -5
- package/.claude/hooks/play-tts-macos.sh +29 -6
- package/.claude/hooks/play-tts-piper.sh +174 -67
- package/.claude/hooks/play-tts-soprano.sh +42 -6
- package/.claude/hooks/play-tts-ssh-remote.sh +117 -38
- package/.claude/hooks/play-tts.sh +12 -9
- package/.claude/hooks/session-start-tts.sh +10 -0
- package/.claude/hooks/stop-tts.sh +84 -0
- package/.claude/hooks/tts-queue-worker.sh +51 -20
- package/.claude/hooks/tts-queue.sh +37 -8
- package/.claude/hooks/voice-manager.sh +5 -1
- package/CLAUDE.md +0 -11
- package/README.md +176 -78
- package/RELEASE_NOTES.md +1197 -60
- package/bin/agentvibes-voice-browser.js +35 -21
- package/mcp-server/server.py +36 -0
- package/package.json +1 -3
- package/src/console/app.js +23 -5
- package/src/console/constants/personalities.js +44 -0
- package/src/console/footer-config.js +8 -0
- package/src/console/navigation.js +3 -1
- package/src/console/tabs/agents-tab.js +1219 -72
- package/src/console/tabs/install-tab.js +2 -1
- package/src/console/tabs/placeholder-tab.js +9 -1
- package/src/console/tabs/receiver-tab.js +1212 -0
- package/src/console/tabs/settings-tab.js +33 -323
- package/src/console/widgets/destroy-list.js +25 -0
- package/src/console/widgets/format-utils.js +89 -0
- package/src/console/widgets/notice.js +55 -0
- package/src/console/widgets/personality-picker.js +185 -0
- package/src/console/widgets/reverb-picker.js +94 -0
- package/src/console/widgets/track-picker.js +285 -0
- package/src/installer.js +54 -2
- package/src/services/agent-voice-store.js +282 -22
- package/src/services/config-service.js +24 -0
- package/src/services/navigation-service.js +1 -1
- package/src/utils/music-file-validator.js +41 -31
- package/templates/agentvibes-receiver.sh +431 -111
|
@@ -242,12 +242,15 @@ class AgentVibesVoiceBrowser {
|
|
|
242
242
|
for (const line of lines) {
|
|
243
243
|
try {
|
|
244
244
|
const voice = JSON.parse(line);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
// SECURITY: Validate expected schema from PowerShell output (#133)
|
|
246
|
+
if (voice && typeof voice.Name === 'string' && voice.Name.length > 0) {
|
|
247
|
+
voices.push({
|
|
248
|
+
name: voice.Name,
|
|
249
|
+
gender: typeof voice.Gender === 'string' ? voice.Gender.toLowerCase() : 'unknown',
|
|
250
|
+
language: typeof voice.Culture === 'string' ? voice.Culture : 'en-US',
|
|
251
|
+
provider: 'windows-sapi'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
251
254
|
} catch {}
|
|
252
255
|
}
|
|
253
256
|
return voices;
|
|
@@ -1309,7 +1312,8 @@ class AgentVibesVoiceBrowser {
|
|
|
1309
1312
|
|
|
1310
1313
|
for (const player of players) {
|
|
1311
1314
|
try {
|
|
1312
|
-
|
|
1315
|
+
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1316
|
+
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1313
1317
|
|
|
1314
1318
|
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
1315
1319
|
this.currentAudioProcess = audioProcess;
|
|
@@ -1508,7 +1512,10 @@ class AgentVibesVoiceBrowser {
|
|
|
1508
1512
|
|
|
1509
1513
|
async playWindowsSAPIVoice(row, sampleText) {
|
|
1510
1514
|
try {
|
|
1511
|
-
|
|
1515
|
+
// SECURITY: Escape row.name and sampleText for PowerShell single-quote context (#124)
|
|
1516
|
+
const safeName = row.name.replace(/'/g, "''");
|
|
1517
|
+
const safeText = sampleText.replace(/'/g, "''");
|
|
1518
|
+
const psScript = `Add-Type -AssemblyName System.Speech; $synth = New-Object System.Speech.Synthesis.SpeechSynthesizer; $synth.SelectVoice('${safeName}'); $synth.Speak('${safeText}')`;
|
|
1512
1519
|
const process = spawn('powershell', ['-Command', psScript], { stdio: 'ignore' });
|
|
1513
1520
|
this.currentAudioProcess = process;
|
|
1514
1521
|
|
|
@@ -1547,9 +1554,18 @@ class AgentVibesVoiceBrowser {
|
|
|
1547
1554
|
};
|
|
1548
1555
|
await fs.writeFile(payloadFile, JSON.stringify(payload));
|
|
1549
1556
|
|
|
1550
|
-
//
|
|
1551
|
-
|
|
1552
|
-
|
|
1557
|
+
// SECURITY: Use spawn with argument array instead of shell string (#125)
|
|
1558
|
+
await new Promise((resolve, reject) => {
|
|
1559
|
+
const curlProc = spawn('curl', [
|
|
1560
|
+
'-s', '-m', '10', '-X', 'POST',
|
|
1561
|
+
'http://127.0.0.1:7860/v1/audio/speech',
|
|
1562
|
+
'-H', 'Content-Type: application/json',
|
|
1563
|
+
'-d', `@${payloadFile}`,
|
|
1564
|
+
'-o', outputFile
|
|
1565
|
+
], { stdio: 'ignore' });
|
|
1566
|
+
curlProc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`curl exited ${code}`)));
|
|
1567
|
+
curlProc.on('error', reject);
|
|
1568
|
+
});
|
|
1553
1569
|
|
|
1554
1570
|
// Clean up payload file
|
|
1555
1571
|
try {
|
|
@@ -1565,7 +1581,8 @@ class AgentVibesVoiceBrowser {
|
|
|
1565
1581
|
|
|
1566
1582
|
for (const player of players) {
|
|
1567
1583
|
try {
|
|
1568
|
-
|
|
1584
|
+
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1585
|
+
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1569
1586
|
|
|
1570
1587
|
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
|
1571
1588
|
this.currentAudioProcess = audioProcess;
|
|
@@ -1630,10 +1647,8 @@ class AgentVibesVoiceBrowser {
|
|
|
1630
1647
|
}
|
|
1631
1648
|
|
|
1632
1649
|
const modelPath = path.join(CONFIG.PIPER_VOICES_DIR, `${safeModel}.onnx`);
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
} catch {
|
|
1636
|
-
// Use spawn instead of execAsync to prevent command injection
|
|
1650
|
+
// SECURITY: Always regenerate instead of TOCTOU check (#132)
|
|
1651
|
+
{
|
|
1637
1652
|
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1638
1653
|
'--model', modelPath,
|
|
1639
1654
|
'--output_file', outputFile
|
|
@@ -1666,10 +1681,8 @@ class AgentVibesVoiceBrowser {
|
|
|
1666
1681
|
return;
|
|
1667
1682
|
}
|
|
1668
1683
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
} catch {
|
|
1672
|
-
// Use spawn instead of execAsync to prevent command injection
|
|
1684
|
+
// SECURITY: Always regenerate instead of TOCTOU check (#132)
|
|
1685
|
+
{
|
|
1673
1686
|
const piperProcess = spawn(CONFIG.PIPER_PATH, [
|
|
1674
1687
|
'--model', CONFIG.MODEL_PATH,
|
|
1675
1688
|
'--speaker', row.id.toString(),
|
|
@@ -1694,7 +1707,8 @@ class AgentVibesVoiceBrowser {
|
|
|
1694
1707
|
|
|
1695
1708
|
for (const player of players) {
|
|
1696
1709
|
try {
|
|
1697
|
-
|
|
1710
|
+
// SECURITY: Use spawnSync instead of shell string (#126)
|
|
1711
|
+
if (spawnSync('which', [player.cmd], { stdio: 'ignore' }).status !== 0) throw new Error('not found');
|
|
1698
1712
|
|
|
1699
1713
|
// SECURITY: Store process immediately to prevent leak
|
|
1700
1714
|
const audioProcess = spawn(player.cmd, player.args, { stdio: 'ignore' });
|
package/mcp-server/server.py
CHANGED
|
@@ -821,6 +821,42 @@ class AgentVibesServer:
|
|
|
821
821
|
result = await self._run_script("clean-audio-cache.sh", [])
|
|
822
822
|
return result if result else "❌ Failed to clean audio cache"
|
|
823
823
|
|
|
824
|
+
async def set_banner(self, enabled: bool) -> str:
|
|
825
|
+
"""
|
|
826
|
+
Enable or disable the TTS output banner (voice info, file path, cache size).
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
enabled: True to show banner, False to hide it
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
Confirmation message
|
|
833
|
+
"""
|
|
834
|
+
banner_file = Path.home() / ".agentvibes" / "banner-disabled"
|
|
835
|
+
if enabled:
|
|
836
|
+
# Remove the disable flag
|
|
837
|
+
try:
|
|
838
|
+
banner_file.unlink(missing_ok=True)
|
|
839
|
+
except Exception:
|
|
840
|
+
pass
|
|
841
|
+
return "✅ TTS banner enabled — voice info will show after each speech"
|
|
842
|
+
else:
|
|
843
|
+
# Create the disable flag
|
|
844
|
+
banner_file.parent.mkdir(parents=True, exist_ok=True)
|
|
845
|
+
banner_file.touch()
|
|
846
|
+
return "🔇 TTS banner disabled — speech will play without output info"
|
|
847
|
+
|
|
848
|
+
async def get_banner(self) -> str:
|
|
849
|
+
"""
|
|
850
|
+
Check if the TTS output banner is enabled or disabled.
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
Current banner status
|
|
854
|
+
"""
|
|
855
|
+
banner_file = Path.home() / ".agentvibes" / "banner-disabled"
|
|
856
|
+
if banner_file.exists():
|
|
857
|
+
return "🔇 TTS banner: **disabled**\n\nSay: \"Turn on banner\" to re-enable"
|
|
858
|
+
return "✅ TTS banner: **enabled**\n\nSay: \"Turn off banner\" to disable"
|
|
859
|
+
|
|
824
860
|
# Helper methods
|
|
825
861
|
def _build_script_env(self) -> dict:
|
|
826
862
|
"""Build environment dict for script execution (shared by all script runners)"""
|
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": "4.
|
|
4
|
+
"version": "4.2.0",
|
|
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": [
|
|
@@ -89,7 +89,6 @@
|
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
91
|
"@inquirer/search": "^3.1.3",
|
|
92
|
-
"agentvibes": "^3.5.9",
|
|
93
92
|
"blessed": "^0.1.81",
|
|
94
93
|
"boxen": "^7.0.0",
|
|
95
94
|
"chalk": "^5.0.0",
|
|
@@ -106,7 +105,6 @@
|
|
|
106
105
|
"access": "public"
|
|
107
106
|
},
|
|
108
107
|
"devDependencies": {
|
|
109
|
-
"bats": "^1.13.0",
|
|
110
108
|
"c8": "^10.1.3"
|
|
111
109
|
}
|
|
112
110
|
}
|
package/src/console/app.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
15
15
|
import { NavigationService, TAB_ORDER } from '../services/navigation-service.js';
|
|
16
16
|
import { setupNavigation } from './navigation.js';
|
|
17
|
-
import { createPlaceholderTab, TAB_DISPLAY_LABELS } from './tabs/placeholder-tab.js';
|
|
17
|
+
import { createPlaceholderTab, TAB_DISPLAY_LABELS, TAB_SHORTCUT_KEYS } from './tabs/placeholder-tab.js';
|
|
18
18
|
import { FOOTER_CONFIG, DEFAULT_FOOTER_COLOR } from './footer-config.js';
|
|
19
19
|
import { createModalOverlay } from './modals/modal-overlay.js';
|
|
20
20
|
import { BRAND_PINK } from './brand-colors.js';
|
|
@@ -24,6 +24,8 @@ import { createMusicTab } from './tabs/music-tab.js';
|
|
|
24
24
|
import { createInstallTab } from './tabs/install-tab.js';
|
|
25
25
|
import { createHelpTab } from './tabs/help-tab.js';
|
|
26
26
|
import { createReadmeTab } from './tabs/readme-tab.js';
|
|
27
|
+
import { createReceiverTab } from './tabs/receiver-tab.js';
|
|
28
|
+
import { createAgentsTab } from './tabs/agents-tab.js';
|
|
27
29
|
import { ConfigService } from '../services/config-service.js';
|
|
28
30
|
import { ProviderService } from '../services/provider-service.js';
|
|
29
31
|
|
|
@@ -112,7 +114,7 @@ export class AgentVibesConsole {
|
|
|
112
114
|
// Screen options stored as property so tests can verify correct configuration
|
|
113
115
|
// without needing to intercept the blessed.screen() call (ESM mock limitation).
|
|
114
116
|
this._screenOptions = {
|
|
115
|
-
smartCSR:
|
|
117
|
+
smartCSR: true,
|
|
116
118
|
mouse: true,
|
|
117
119
|
fullUnicode: true,
|
|
118
120
|
title: `AgentVibes v${APP_VERSION} TUI Console`,
|
|
@@ -292,7 +294,8 @@ export class AgentVibesConsole {
|
|
|
292
294
|
let xOffset = 1;
|
|
293
295
|
for (const id of TAB_ORDER) {
|
|
294
296
|
const label = TAB_DISPLAY_LABELS[id];
|
|
295
|
-
const
|
|
297
|
+
const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
|
|
298
|
+
const text = ` [${shortcutKey}] ${label} `;
|
|
296
299
|
const el = blessed.box({
|
|
297
300
|
parent: this.screen,
|
|
298
301
|
top: 3,
|
|
@@ -478,10 +481,11 @@ export class AgentVibesConsole {
|
|
|
478
481
|
_renderTabBarContent(activeTabId) {
|
|
479
482
|
return TAB_ORDER.map(id => {
|
|
480
483
|
const label = TAB_DISPLAY_LABELS[id];
|
|
484
|
+
const shortcutKey = TAB_SHORTCUT_KEYS[id] || label[0];
|
|
481
485
|
if (id === activeTabId) {
|
|
482
|
-
return `{bold}{white-fg}[${
|
|
486
|
+
return `{bold}{white-fg}[${shortcutKey}] ${label}{/white-fg}{/bold}`;
|
|
483
487
|
}
|
|
484
|
-
return `{#82b1ff-fg}[${
|
|
488
|
+
return `{#82b1ff-fg}[${shortcutKey}] ${label}{/#82b1ff-fg}`;
|
|
485
489
|
}).join(' ');
|
|
486
490
|
}
|
|
487
491
|
|
|
@@ -657,6 +661,20 @@ export class AgentVibesConsole {
|
|
|
657
661
|
}
|
|
658
662
|
this.tabs['help'] = createHelpTab(this.screen, services);
|
|
659
663
|
|
|
664
|
+
// Destroy agents placeholder and mount real agents tab
|
|
665
|
+
const agentsPlaceholder = this.tabs['agents'];
|
|
666
|
+
if (agentsPlaceholder && typeof agentsPlaceholder.destroy === 'function') {
|
|
667
|
+
agentsPlaceholder.destroy();
|
|
668
|
+
}
|
|
669
|
+
this.tabs['agents'] = createAgentsTab(this.screen, services);
|
|
670
|
+
|
|
671
|
+
// Destroy receiver placeholder and mount real receiver tab
|
|
672
|
+
const receiverPlaceholder = this.tabs['receiver'];
|
|
673
|
+
if (receiverPlaceholder && typeof receiverPlaceholder.destroy === 'function') {
|
|
674
|
+
receiverPlaceholder.destroy();
|
|
675
|
+
}
|
|
676
|
+
this.tabs['receiver'] = createReceiverTab(this.screen, services);
|
|
677
|
+
|
|
660
678
|
const readmePlaceholder = this.tabs['readme'];
|
|
661
679
|
if (readmePlaceholder && typeof readmePlaceholder.destroy === 'function') {
|
|
662
680
|
readmePlaceholder.destroy();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentVibes — Canonical personality constants.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for personality names and their associated emoji
|
|
5
|
+
* glyphs. All TUI modules (settings-tab, agents-tab, personality-picker …)
|
|
6
|
+
* import from here; src/installer.js maintains its own copy because it
|
|
7
|
+
* predates the TUI and uses a different module-load path.
|
|
8
|
+
*
|
|
9
|
+
* Exported:
|
|
10
|
+
* PERSONALITY_EMOJIS — Map of personality name → emoji string
|
|
11
|
+
* PERSONALITIES — Ordered array of personality names (canonical order
|
|
12
|
+
* used for picker lists)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const PERSONALITY_EMOJIS = Object.freeze({
|
|
16
|
+
angry: '😠',
|
|
17
|
+
annoying: '😤',
|
|
18
|
+
crass: '🤬',
|
|
19
|
+
dramatic: '🎭',
|
|
20
|
+
'dry-humor': '😐',
|
|
21
|
+
flirty: '😘',
|
|
22
|
+
funny: '😂',
|
|
23
|
+
grandpa: '👴',
|
|
24
|
+
millennial: '🙄',
|
|
25
|
+
moody: '😒',
|
|
26
|
+
none: '😊',
|
|
27
|
+
normal: '😊',
|
|
28
|
+
pirate: '⚓',
|
|
29
|
+
poetic: '📜',
|
|
30
|
+
professional: '👔',
|
|
31
|
+
rapper: '🎤',
|
|
32
|
+
robot: '🤖',
|
|
33
|
+
sarcastic: '😏',
|
|
34
|
+
sassy: '💁',
|
|
35
|
+
'surfer-dude':'🏄',
|
|
36
|
+
zen: '🧘',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const PERSONALITIES = Object.freeze([
|
|
40
|
+
'none', 'angry', 'annoying', 'crass', 'dramatic', 'dry-humor',
|
|
41
|
+
'flirty', 'funny', 'grandpa', 'millennial', 'moody', 'normal',
|
|
42
|
+
'pirate', 'poetic', 'professional', 'rapper', 'robot', 'sarcastic',
|
|
43
|
+
'sassy', 'surfer-dude', 'zen',
|
|
44
|
+
]);
|
|
@@ -35,6 +35,14 @@ export const FOOTER_CONFIG = {
|
|
|
35
35
|
color: '#607d8b',
|
|
36
36
|
text: ` ${key('↑↓')} Scroll ${key('Q')} Quit`,
|
|
37
37
|
},
|
|
38
|
+
agents: {
|
|
39
|
+
color: '#9c27b0',
|
|
40
|
+
text: ` ${key('↑↓')} Navigate ${key('Enter')} Edit Agent ${key('Space')} Sample ${key('R')} Reset`,
|
|
41
|
+
},
|
|
42
|
+
receiver: {
|
|
43
|
+
color: '#00897b',
|
|
44
|
+
text: ` ${key('E')} Enable ${key('D')} Details ${key('C')} Clear Log`,
|
|
45
|
+
},
|
|
38
46
|
install: {
|
|
39
47
|
color: '#1a237e',
|
|
40
48
|
text: ` ${key('↑↓')} Navigate ${key('Enter')} Select ${key('Esc')} Back`,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Story 6.2: Tab Bar & Global Keyboard Navigation
|
|
4
4
|
*
|
|
5
5
|
* Registers all global key bindings on the Blessed screen.
|
|
6
|
-
* Tab shortcuts (S/V/M/
|
|
6
|
+
* Tab shortcuts (S/V/M/X/R/H/I) are blocked when a modal is open.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** Map of key → tab ID for global tab shortcut keys */
|
|
@@ -11,6 +11,8 @@ const KEY_TO_TAB = {
|
|
|
11
11
|
's': 'settings', 'S': 'settings',
|
|
12
12
|
'v': 'voices', 'V': 'voices',
|
|
13
13
|
'm': 'music', 'M': 'music',
|
|
14
|
+
'b': 'agents', 'B': 'agents',
|
|
15
|
+
'x': 'receiver', 'X': 'receiver',
|
|
14
16
|
'r': 'readme', 'R': 'readme',
|
|
15
17
|
'h': 'help', 'H': 'help',
|
|
16
18
|
'i': 'install', 'I': 'install',
|