agentvibes 5.13.1 → 5.15.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.
Files changed (84) hide show
  1. package/.claude/audio/ui/CREDITS.txt +16 -16
  2. package/.claude/commands/agent-vibes-bmad-voices.md +117 -117
  3. package/.claude/commands/agent-vibes-rdp.md +24 -24
  4. package/.claude/config/audio-effects.cfg.sample +52 -52
  5. package/.claude/docs/TERMUX_SETUP.md +408 -408
  6. package/.claude/github-star-reminder.txt +1 -1
  7. package/.claude/hooks/kokoro-server.py +219 -219
  8. package/.claude/hooks/kokoro-tts.py +141 -141
  9. package/.claude/hooks/party-stage-roster.py +328 -328
  10. package/.claude/hooks/play-tts-kokoro.sh +7 -2
  11. package/.claude/hooks/play-tts-ssh-remote.sh +38 -3
  12. package/.claude/hooks/requirements.txt +6 -6
  13. package/.claude/hooks/soprano-gradio-synth.py +139 -139
  14. package/.claude/hooks/translator.py +237 -237
  15. package/.claude/hooks-windows/agentvibes-session-id.ps1 +83 -0
  16. package/.claude/hooks-windows/kokoro-server.py +219 -219
  17. package/.claude/hooks-windows/kokoro-tts.py +107 -107
  18. package/.claude/hooks-windows/play-tts.ps1 +112 -1
  19. package/.claude/hooks-windows/session-start-tts.ps1 +36 -0
  20. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  21. package/.claude/verbosity.txt +1 -1
  22. package/.clawdbot/README.md +105 -105
  23. package/CLAUDE.md +84 -176
  24. package/README.md +35 -2
  25. package/RELEASE_NOTES.md +139 -0
  26. package/WINDOWS-SETUP.md +208 -208
  27. package/bin/agent-vibes +39 -39
  28. package/bin/mcp-server.js +121 -121
  29. package/bin/test-bmad-pr +78 -78
  30. package/mcp-server/QUICK_START.md +203 -203
  31. package/mcp-server/README.md +345 -345
  32. package/mcp-server/examples/claude_desktop_config.json +11 -11
  33. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  34. package/mcp-server/examples/custom_instructions.md +169 -169
  35. package/mcp-server/install-deps.js +12 -2
  36. package/mcp-server/server.py +2111 -2085
  37. package/mcp-server/test_mcp_correctness.py +504 -504
  38. package/mcp-server/test_windows_script_parity.py +339 -339
  39. package/package.json +6 -3
  40. package/setup-ssh-receiver.ps1 +259 -0
  41. package/setup-windows.ps1 +39 -29
  42. package/src/cli/list-personalities.js +110 -110
  43. package/src/commands/bmad-voices.js +394 -394
  44. package/src/console/brand-colors.js +13 -13
  45. package/src/console/constants/personalities.js +44 -44
  46. package/src/console/tabs/agents-tab.js +14 -4
  47. package/src/console/tabs/help-tab.js +314 -314
  48. package/src/console/tabs/readme-tab.js +272 -272
  49. package/src/console/tabs/settings-tab.js +31 -6
  50. package/src/console/tabs/setup-tab.js +154 -32
  51. package/src/console/tabs/voices-tab.js +48 -6
  52. package/src/console/widgets/destroy-list.js +25 -25
  53. package/src/console/widgets/notice.js +55 -55
  54. package/src/installer/language-screen.js +31 -31
  55. package/src/installer/music-file-input.js +304 -304
  56. package/src/installer.js +98 -37
  57. package/src/services/language-service.js +47 -47
  58. package/src/services/llm-provider-service.js +4 -1
  59. package/src/services/provider-voice-catalog.js +9 -5
  60. package/src/utils/audio-format-validator.js +277 -277
  61. package/src/utils/dependency-checker.js +469 -469
  62. package/src/utils/file-ownership-verifier.js +358 -358
  63. package/src/utils/music-file-validator.js +285 -285
  64. package/src/utils/preview-list-prompt.js +144 -144
  65. package/src/utils/secure-music-storage.js +412 -412
  66. package/templates/agentvibes-receiver.ps1 +311 -0
  67. package/templates/agentvibes-receiver.sh +297 -297
  68. package/voice-assignments.json +8244 -8244
  69. package/.agentvibes/config.json +0 -17
  70. package/.agentvibes/install-manifest.json +0 -362
  71. package/.claude/config/audio-effects.cfg +0 -7
  72. package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
  73. package/.claude/config/background-music-enabled.txt +0 -1
  74. package/.claude/config/background-music-position.txt +0 -27
  75. package/.claude/config/background-music-volume.txt +0 -1
  76. package/.claude/config/background-music.cfg +0 -1
  77. package/.claude/config/background-music.txt +0 -1
  78. package/.claude/config/language.txt +0 -1
  79. package/.claude/config/personality.txt +0 -1
  80. package/.claude/config/reverb-level.txt +0 -1
  81. package/.claude/config/tts-speech-rate.txt +0 -1
  82. package/.claude/config/tts-verbosity.txt +0 -1
  83. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  84. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
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.13.1",
4
+ "version": "5.15.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": [
@@ -37,6 +37,7 @@
37
37
  "bin/",
38
38
  "src/",
39
39
  "templates/*.sh",
40
+ "templates/*.ps1",
40
41
  "templates/*.md",
41
42
  "templates/audio/*.mp3",
42
43
  "mcp-server/*.py",
@@ -56,7 +57,7 @@
56
57
  ".claude/output-styles/",
57
58
  ".claude/audio/tracks/",
58
59
  ".claude/audio/ui/",
59
- ".claude/config/",
60
+ ".claude/config/*.sample",
60
61
  ".claude/docs/",
61
62
  ".claude/language-voices.yaml",
62
63
  ".claude/settings.json",
@@ -65,10 +66,12 @@
65
66
  ".claude/verbosity.txt",
66
67
  ".claude/github-star-reminder.txt",
67
68
  ".claude/audio/voice-samples/",
68
- ".agentvibes/",
69
+ ".agentvibes/bmad/",
69
70
  ".clawdbot/",
70
71
  ".mcp.json",
72
+ "!**/*.user.bak",
71
73
  "setup-windows.ps1",
74
+ "setup-ssh-receiver.ps1",
72
75
  "WINDOWS-SETUP.md",
73
76
  "README.md",
74
77
  "RELEASE_NOTES.md",
@@ -0,0 +1,259 @@
1
+ #Requires -RunAsAdministrator
2
+ #
3
+ # AgentVibes SSH Receiver Setup (Windows)
4
+ # Run this in an ADMIN PowerShell:
5
+ # powershell -ExecutionPolicy Bypass -File setup-ssh-receiver.ps1
6
+ #
7
+
8
+ Write-Host "`n=== AgentVibes SSH Receiver Setup ===" -ForegroundColor Cyan
9
+ Write-Host ""
10
+
11
+ $Username = $env:USERNAME
12
+ $TailscaleIp = ""
13
+ try {
14
+ $TailscaleIp = (tailscale ip -4 2>$null).Trim()
15
+ } catch {}
16
+
17
+ if (-not $TailscaleIp) {
18
+ Write-Host "WARNING: Tailscale not found. Install Tailscale first for secure setup." -ForegroundColor Yellow
19
+ Write-Host " Falling back to listen on all interfaces." -ForegroundColor Yellow
20
+ $TailscaleIp = "0.0.0.0"
21
+ }
22
+
23
+ # Step 1: Stop sshd
24
+ Write-Host "[1/8] Stopping sshd..." -ForegroundColor Yellow
25
+ Stop-Service sshd -ErrorAction SilentlyContinue
26
+
27
+ # Step 2: Backup original config
28
+ Write-Host "[2/8] Backing up original sshd_config..." -ForegroundColor Yellow
29
+ $configPath = "C:\ProgramData\ssh\sshd_config"
30
+ $backupPath = "C:\ProgramData\ssh\sshd_config.bak"
31
+ if (Test-Path $configPath) {
32
+ # Only create the backup once — never overwrite it, so the user's original
33
+ # sshd_config is always recoverable even if this script is run multiple times.
34
+ if (-not (Test-Path $backupPath)) {
35
+ Copy-Item $configPath $backupPath
36
+ Write-Host " Backup: $backupPath" -ForegroundColor Gray
37
+ } else {
38
+ Write-Host " Backup already exists: $backupPath (skipped)" -ForegroundColor DarkGray
39
+ }
40
+ }
41
+
42
+ # Step 3: Deploy hardened config with user-specific values
43
+ Write-Host "[3/8] Deploying hardened sshd_config..." -ForegroundColor Yellow
44
+ $hardenedConfig = Join-Path $PSScriptRoot "templates\sshd_config_hardened"
45
+ if (Test-Path $hardenedConfig) {
46
+ $config = Get-Content $hardenedConfig -Raw
47
+ $config = $config -replace 'YourUsername', $Username
48
+ $config = $config -replace 'ListenAddress 0\.0\.0\.0', "ListenAddress $TailscaleIp"
49
+ $receiverPath = "C:\Users\$Username\.agentvibes\play-remote.ps1"
50
+ $config = $config -replace 'C:\\Users\\YourUsername\\\.agentvibes\\play-remote\.ps1', $receiverPath
51
+ Set-Content -Path $configPath -Value $config
52
+ Write-Host " Port: 45123" -ForegroundColor Green
53
+ Write-Host " Listen: $TailscaleIp" -ForegroundColor Green
54
+ Write-Host " Auth: Key-only, no passwords" -ForegroundColor Green
55
+ Write-Host " ForceCommand: Receiver script only, no shell" -ForegroundColor Green
56
+ } else {
57
+ Write-Host " ERROR: $hardenedConfig not found!" -ForegroundColor Red
58
+ exit 1
59
+ }
60
+
61
+ # Step 4: Install receiver script
62
+ Write-Host "[4/8] Installing receiver script..." -ForegroundColor Yellow
63
+ $agentvibesDir = "$env:USERPROFILE\.agentvibes"
64
+ if (-not (Test-Path $agentvibesDir)) {
65
+ New-Item -ItemType Directory -Path $agentvibesDir -Force | Out-Null
66
+ }
67
+ $receiverSrc = Join-Path $PSScriptRoot "templates\agentvibes-receiver.ps1"
68
+ if (Test-Path $receiverSrc) {
69
+ # Stamp the installing user's home path so the receiver can find hooks/config
70
+ # even when sshd runs it as a different user (e.g. agentvibes-receiver)
71
+ $content = Get-Content $receiverSrc -Raw
72
+ $content = $content -replace '__OWNER_HOME__', $env:USERPROFILE
73
+ Set-Content -Path "$agentvibesDir\play-remote.ps1" -Value $content
74
+ Write-Host " Installed to: $agentvibesDir\play-remote.ps1" -ForegroundColor Green
75
+ Write-Host " Owner home: $env:USERPROFILE" -ForegroundColor Gray
76
+ }
77
+
78
+ # Step 5: Grant agentvibes-receiver read access to required paths
79
+ Write-Host "[5/8] Granting agentvibes-receiver permissions..." -ForegroundColor Yellow
80
+ $ReceiverUser = "agentvibes-receiver"
81
+ $userExists = Get-LocalUser -Name $ReceiverUser -ErrorAction SilentlyContinue
82
+ if ($userExists) {
83
+ # Read+Execute on .agentvibes
84
+ icacls "$agentvibesDir" /grant "${ReceiverUser}:(OI)(CI)RX" /T /Q 2>$null
85
+ # Modify on tts-queue (receiver writes request files) - create if absent
86
+ $queueDir = "$agentvibesDir\tts-queue"
87
+ if (-not (Test-Path $queueDir)) {
88
+ New-Item -ItemType Directory -Path $queueDir -Force | Out-Null
89
+ }
90
+ icacls "$queueDir" /grant "${ReceiverUser}:(OI)(CI)M" /T /Q 2>$null
91
+ # Modify on receiver.log - touch if absent so icacls can target it
92
+ $logFile = "$agentvibesDir\receiver.log"
93
+ if (-not (Test-Path $logFile)) {
94
+ New-Item -ItemType File -Path $logFile -Force | Out-Null
95
+ }
96
+ icacls "$logFile" /grant "${ReceiverUser}:M" /Q 2>$null
97
+ # Targeted permissions on .claude subdirectories (not blanket Modify on all of .claude)
98
+ $claudeDir = "$env:USERPROFILE\.claude"
99
+ if (Test-Path $claudeDir) {
100
+ # RX on .claude root (needed for Test-Path checks)
101
+ icacls "$claudeDir" /grant "${ReceiverUser}:RX" /Q 2>$null
102
+ # RX on hooks (read + execute scripts)
103
+ $hooksDir = "$claudeDir\hooks-windows"
104
+ if (Test-Path $hooksDir) {
105
+ icacls "$hooksDir" /grant "${ReceiverUser}:(OI)(CI)RX" /T /Q 2>$null
106
+ }
107
+ # Modify on config (receiver writes voice, speed, music settings)
108
+ $configDir = "$claudeDir\config"
109
+ if (Test-Path $configDir) {
110
+ icacls "$configDir" /grant "${ReceiverUser}:(OI)(CI)M" /T /Q 2>$null
111
+ }
112
+ # Modify on audio output dir (receiver writes WAV files)
113
+ $audioDir = "$claudeDir\audio"
114
+ if (-not (Test-Path $audioDir)) {
115
+ New-Item -ItemType Directory -Path $audioDir -Force | Out-Null
116
+ }
117
+ icacls "$audioDir" /grant "${ReceiverUser}:(OI)(CI)M" /T /Q 2>$null
118
+ # Modify on tts-voice.txt and tts-provider.txt (receiver reads/writes these)
119
+ foreach ($txt in @("tts-voice.txt", "tts-provider.txt")) {
120
+ $f = "$claudeDir\$txt"
121
+ if (Test-Path $f) {
122
+ icacls "$f" /grant "${ReceiverUser}:M" /Q 2>$null
123
+ }
124
+ }
125
+ }
126
+ # Read+Execute on Piper install
127
+ $piperDir = "$env:LOCALAPPDATA\Programs\Piper"
128
+ if (Test-Path $piperDir) {
129
+ icacls "$piperDir" /grant "${ReceiverUser}:(OI)(CI)RX" /T /Q 2>$null
130
+ }
131
+ Write-Host " Granted $ReceiverUser access to .agentvibes, .claude, Piper" -ForegroundColor Green
132
+ } else {
133
+ Write-Host " SKIP: $ReceiverUser user not found (create it first)" -ForegroundColor Yellow
134
+ }
135
+
136
+ # Step 6: Firewall rule
137
+ Write-Host "[6/8] Adding firewall rule for port 45123 (Tailscale only)..." -ForegroundColor Yellow
138
+ $existingRule = Get-NetFirewallRule -Name "AgentVibes-SSH-Receiver" -ErrorAction SilentlyContinue
139
+ if ($existingRule) {
140
+ Remove-NetFirewallRule -Name "AgentVibes-SSH-Receiver"
141
+ }
142
+ New-NetFirewallRule -Name "AgentVibes-SSH-Receiver" `
143
+ -DisplayName "AgentVibes SSH Receiver" `
144
+ -Description "Allow AgentVibes TTS receiver on port 45123 from Tailscale only" `
145
+ -Direction Inbound `
146
+ -Protocol TCP `
147
+ -LocalPort 45123 `
148
+ -RemoteAddress 100.0.0.0/8 `
149
+ -Action Allow | Out-Null
150
+ Write-Host " Firewall: Allow TCP 45123 from 100.0.0.0/8 only" -ForegroundColor Green
151
+
152
+ # Step 7: Set up admin authorized keys
153
+ Write-Host "[7/8] Configuring authorized keys..." -ForegroundColor Yellow
154
+ $adminKeysFile = "C:\ProgramData\ssh\administrators_authorized_keys"
155
+ if (-not (Test-Path $adminKeysFile)) {
156
+ Write-Host " NOTE: No SSH keys found at $adminKeysFile" -ForegroundColor Yellow
157
+ Write-Host " Add your sender's public key there, then run:" -ForegroundColor Yellow
158
+ Write-Host " icacls `"$adminKeysFile`" /inheritance:r /grant `"SYSTEM:F`" /grant `"BUILTIN\Administrators:F`"" -ForegroundColor Cyan
159
+ } else {
160
+ & icacls $adminKeysFile /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F" 2>$null
161
+ }
162
+ # Step 7.5: Install user-session watcher (audio device access)
163
+ # CRITICAL: SSH receiver runs in session 0 (no audio). The receiver writes
164
+ # to ~/.agentvibes/tts-queue/, and a watcher running in the user's
165
+ # interactive session picks up requests and plays audio.
166
+ Write-Host "[7.5/8] Installing user-session watcher..." -ForegroundColor Yellow
167
+ # Copy watcher from repo source — single source of truth, no embedded heredoc
168
+ $sourceWatcher = Join-Path $PSScriptRoot ".claude\hooks-windows\tts-watcher.ps1"
169
+ if (-not (Test-Path $sourceWatcher)) {
170
+ Write-Host " ERROR: $sourceWatcher not found — run from the AgentVibes repo root" -ForegroundColor Red
171
+ exit 1
172
+ }
173
+ $watcherDestDir = "$env:USERPROFILE\.agentvibes"
174
+ if (-not (Test-Path $watcherDestDir)) { New-Item -ItemType Directory -Path $watcherDestDir -Force | Out-Null }
175
+ Copy-Item -Path $sourceWatcher -Destination "$watcherDestDir\tts-watcher.ps1" -Force
176
+
177
+ $vbsLauncher = @'
178
+ Set WshShell = CreateObject("WScript.Shell")
179
+ WshShell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File """ & WshShell.ExpandEnvironmentStrings("%USERPROFILE%") & "\.agentvibes\tts-watcher.ps1""", 0, False
180
+ '@
181
+ Set-Content -Path "$env:USERPROFILE\.agentvibes\start-watcher.vbs" -Value $vbsLauncher -Encoding ASCII
182
+
183
+ # Install autostart shortcut in Startup folder
184
+ $startupDir = [Environment]::GetFolderPath('Startup')
185
+ Copy-Item -Path "$env:USERPROFILE\.agentvibes\start-watcher.vbs" -Destination "$startupDir\agentvibes-watcher.vbs" -Force
186
+
187
+ # Kill any existing watcher instances before launching - prevents double-playback
188
+ # if setup is run more than once (each run would otherwise add another watcher process).
189
+ $existingWatchers = Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*tts-watcher.ps1*" -and $_.Name -like "powershell*" }
190
+ if ($existingWatchers) {
191
+ $existingWatchers | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
192
+ Start-Sleep -Milliseconds 300 # Wait for processes to exit
193
+ Write-Host " Stopped $($existingWatchers.Count) existing watcher(s)" -ForegroundColor DarkYellow
194
+ }
195
+
196
+ # Launch it now so streaming works without requiring logout/login
197
+ Start-Process wscript.exe -ArgumentList "$env:USERPROFILE\.agentvibes\start-watcher.vbs" -WindowStyle Hidden
198
+ Write-Host " Watcher: installed + started + autostart enabled" -ForegroundColor Green
199
+
200
+ # Step 7.6: Install Kokoro daemon autostart (only when Kokoro is available)
201
+ # The Kokoro provider synthesizes via a resident HTTP daemon (kokoro-server.py)
202
+ # that keeps the model on the GPU. Without it, the FIRST receiver message after
203
+ # every reboot pays a ~12s cold model-load. A Startup-folder .vbs keeps it warm.
204
+ Write-Host "[7.6/8] Installing Kokoro daemon autostart (if Kokoro installed)..." -ForegroundColor Yellow
205
+ $pythonCmd = Get-Command python -ErrorAction SilentlyContinue
206
+ $kokoroAvailable = $false
207
+ if ($pythonCmd) {
208
+ # find_spec avoids importing torch (slow); exit 0 => kokoro importable
209
+ & $pythonCmd.Source -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('kokoro') else 1)" 2>$null
210
+ $kokoroAvailable = ($LASTEXITCODE -eq 0)
211
+ }
212
+ if ($kokoroAvailable) {
213
+ $pythonwPath = $pythonCmd.Source -replace 'python\.exe$', 'pythonw.exe'
214
+ if (-not (Test-Path $pythonwPath)) { $pythonwPath = $pythonCmd.Source } # fall back to python.exe
215
+ $kokoroVbs = @"
216
+ ' AgentVibes - Kokoro TTS daemon launcher (autostart)
217
+ ' Keeps the Kokoro model resident on the GPU so receiver TTS latency stays low.
218
+ Set WshShell = CreateObject("WScript.Shell")
219
+ pythonw = "$pythonwPath"
220
+ script = WshShell.ExpandEnvironmentStrings("%USERPROFILE%") & "\.claude\hooks-windows\kokoro-server.py"
221
+ WshShell.Run """" & pythonw & """ """ & script & """ 7855", 0, False
222
+ "@
223
+ Set-Content -Path "$env:USERPROFILE\.agentvibes\start-kokoro-server.vbs" -Value $kokoroVbs -Encoding ASCII
224
+ Copy-Item -Path "$env:USERPROFILE\.agentvibes\start-kokoro-server.vbs" -Destination "$startupDir\agentvibes-kokoro-server.vbs" -Force
225
+ # Kill only the managed daemon (this script's kokoro-server.py on the standard
226
+ # port 7855), then launch a fresh one so it's warm immediately. Scoping by the
227
+ # managed port avoids killing unrelated user-run kokoro-server.py daemons.
228
+ Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*kokoro-server.py*7855*" } |
229
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
230
+ Start-Process wscript.exe -ArgumentList "$env:USERPROFILE\.agentvibes\start-kokoro-server.vbs" -WindowStyle Hidden
231
+ Write-Host " Kokoro daemon: installed + started + autostart enabled (pythonw: $pythonwPath)" -ForegroundColor Green
232
+ } else {
233
+ Write-Host " SKIP: Kokoro not installed (pip install kokoro soundfile numpy to enable)" -ForegroundColor DarkYellow
234
+ }
235
+
236
+ # Step 8: Start sshd
237
+ Write-Host "[8/8] Starting sshd..." -ForegroundColor Yellow
238
+ Start-Service sshd
239
+ $status = (Get-Service sshd).Status
240
+ if ($status -eq "Running") {
241
+ Write-Host " sshd: Running" -ForegroundColor Green
242
+ } else {
243
+ Write-Host " sshd: $status (check Event Viewer)" -ForegroundColor Red
244
+ exit 1
245
+ }
246
+
247
+ Write-Host ""
248
+ Write-Host "=== Setup Complete ===" -ForegroundColor Cyan
249
+ Write-Host ""
250
+ Write-Host "Security:" -ForegroundColor White
251
+ Write-Host " Port: 45123" -ForegroundColor Gray
252
+ Write-Host " Interface: $TailscaleIp (Tailscale)" -ForegroundColor Gray
253
+ Write-Host " Auth: SSH key only" -ForegroundColor Gray
254
+ Write-Host " Access: Receiver script only (no shell)" -ForegroundColor Gray
255
+ Write-Host " Firewall: Tailscale IPs only (100.x.x.x)" -ForegroundColor Gray
256
+ Write-Host ""
257
+ Write-Host "To test from sender:" -ForegroundColor White
258
+ Write-Host " echo `"hello`" | base64 | ssh -p 45123 $Username@$TailscaleIp" -ForegroundColor Cyan
259
+ Write-Host ""
package/setup-windows.ps1 CHANGED
@@ -322,41 +322,51 @@ if (-not (Test-Path $HooksDir)) {
322
322
  New-Item -ItemType Directory -Path $HooksDir -Force | Out-Null
323
323
  }
324
324
 
325
- # Script info: filename, description
326
- $HookScriptInfo = @(
327
- @{ Name = "play-tts.ps1"; Desc = "Main TTS router - dispatches to active provider" },
328
- @{ Name = "play-tts-soprano.ps1"; Desc = "Soprano neural voice provider (fastest)" },
329
- @{ Name = "play-tts-windows-piper.ps1"; Desc = "Piper offline neural voice provider" },
330
- @{ Name = "play-tts-windows-sapi.ps1"; Desc = "Windows built-in SAPI voice provider" },
331
- @{ Name = "provider-manager.ps1"; Desc = "Switch between TTS providers" },
332
- @{ Name = "voice-manager-windows.ps1"; Desc = "Browse and select voice models" },
333
- @{ Name = "audio-cache-utils.ps1"; Desc = "Manage TTS audio file cache" },
334
- @{ Name = "session-start-tts.ps1"; Desc = "Auto-activates TTS when Claude starts" }
335
- )
336
-
325
+ # Friendly descriptions for the well-known scripts; any other *.ps1 in the
326
+ # source dir still gets installed (with a generic label) rather than dropped.
327
+ $HookDescriptions = @{
328
+ "play-tts.ps1" = "Main TTS router - dispatches to active provider"
329
+ "play-tts-soprano.ps1" = "Soprano neural voice provider (fastest)"
330
+ "play-tts-windows-piper.ps1" = "Piper offline neural voice provider"
331
+ "play-tts-windows-sapi.ps1" = "Windows built-in SAPI voice provider"
332
+ "provider-manager.ps1" = "Switch between TTS providers"
333
+ "voice-manager-windows.ps1" = "Browse and select voice models"
334
+ "audio-cache-utils.ps1" = "Manage TTS audio file cache"
335
+ "session-start-tts.ps1" = "Auto-activates TTS when Claude starts"
336
+ }
337
+
338
+ # One timestamp per run so this run's backups group together and never collide.
339
+ $BackupStamp = (Get-Date -Format 'yyyyMMdd-HHmmss')
340
+
341
+ # Copy EVERY hook script, not a hand-maintained whitelist — a partial list
342
+ # produced mixed-generation installs (new router + stale providers/watcher).
337
343
  $CopiedCount = 0
338
- foreach ($info in $HookScriptInfo) {
339
- $script = $info.Name
340
- $SourceFile = Join-Path $SourceHooksDir $script
344
+ foreach ($SourceFile in (Get-ChildItem -Path $SourceHooksDir -Filter *.ps1 -File)) {
345
+ $script = $SourceFile.Name
341
346
  $DestFile = Join-Path $HooksDir $script
347
+ $desc = if ($HookDescriptions.ContainsKey($script)) { $HookDescriptions[$script] } else { "AgentVibes hook script" }
342
348
 
343
- if (Test-Path $SourceFile) {
344
- # Skip if source and destination are the same file (running from project root)
345
- $resolvedSrc = (Resolve-Path $SourceFile).Path
346
- $resolvedDst = if (Test-Path $DestFile) { (Resolve-Path $DestFile).Path } else { "" }
347
- if ($resolvedSrc -eq $resolvedDst) {
348
- Write-Item "[OK]" $script $info.Desc
349
- } else {
350
- Copy-Item -Path $SourceFile -Destination $DestFile -Force
351
- Write-Item "[OK]" $script $info.Desc
352
- }
349
+ # Skip if source and destination are the same file (running from project root)
350
+ $resolvedSrc = $SourceFile.FullName
351
+ $resolvedDst = if (Test-Path $DestFile) { (Resolve-Path $DestFile).Path } else { "" }
352
+ if ($resolvedSrc -eq $resolvedDst) {
353
+ Write-Item "[OK]" $script $desc
353
354
  $CopiedCount++
355
+ continue
354
356
  }
355
- else {
356
- Write-Host " [!!] " -ForegroundColor Yellow -NoNewline
357
- Write-Host "$($script.PadRight(30))" -ForegroundColor White -NoNewline
358
- Write-Host "not found" -ForegroundColor Yellow
357
+
358
+ # Non-Destructive rule: back up an existing, differing file before overwrite
359
+ # so a user-modified hook is always recoverable (timestamped, every run).
360
+ if (Test-Path $DestFile) {
361
+ $srcHash = (Get-FileHash -Path $resolvedSrc -Algorithm SHA256).Hash
362
+ $dstHash = (Get-FileHash -Path $DestFile -Algorithm SHA256).Hash
363
+ if ($srcHash -ne $dstHash) {
364
+ Copy-Item -Path $DestFile -Destination "$DestFile.user.bak.$BackupStamp" -ErrorAction SilentlyContinue
365
+ }
359
366
  }
367
+ Copy-Item -Path $SourceFile.FullName -Destination $DestFile -Force
368
+ Write-Item "[OK]" $script $desc
369
+ $CopiedCount++
360
370
  }
361
371
 
362
372
  if ($CopiedCount -eq 0) {
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Personality List Display - Beautiful multi-column personality listing
4
- * Called by personality-manager.sh to display personalities with boxen formatting
5
- */
6
-
7
- import { formatPersonalitiesList } from '../utils/list-formatter.js';
8
- import fs from 'fs';
9
- import path from 'path';
10
- import os from 'os';
11
-
12
- /**
13
- * Get personality description from markdown file
14
- */
15
- function getPersonalityDescription(filePath) {
16
- try {
17
- const content = fs.readFileSync(filePath, 'utf8');
18
-
19
- // Try to extract description from frontmatter or first paragraph
20
- const descMatch = content.match(/description:\s*(.+)/i);
21
- if (descMatch) {
22
- return descMatch[1].trim();
23
- }
24
-
25
- // Try to get first line after frontmatter
26
- const lines = content.split('\n');
27
- let inFrontmatter = false;
28
- let frontmatterCount = 0;
29
-
30
- for (const line of lines) {
31
- if (line.trim() === '---') {
32
- frontmatterCount++;
33
- inFrontmatter = frontmatterCount === 1;
34
- continue;
35
- }
36
-
37
- if (!inFrontmatter && frontmatterCount >= 2 && line.trim()) {
38
- // First non-empty line after frontmatter
39
- return line.trim().replace(/^#+\s*/, '').substring(0, 50);
40
- }
41
- }
42
-
43
- return '';
44
- } catch (error) {
45
- return '';
46
- }
47
- }
48
-
49
- /**
50
- * Get all personalities from directory
51
- */
52
- function getPersonalities(personalitiesDir, currentPersonality) {
53
- const personalities = [];
54
-
55
- if (!fs.existsSync(personalitiesDir)) {
56
- return personalities;
57
- }
58
-
59
- const files = fs.readdirSync(personalitiesDir);
60
- for (const file of files) {
61
- if (file.endsWith('.md')) {
62
- const name = path.basename(file, '.md');
63
- const filePath = path.join(personalitiesDir, file);
64
- const description = getPersonalityDescription(filePath);
65
-
66
- personalities.push({
67
- name,
68
- description,
69
- current: name === currentPersonality
70
- });
71
- }
72
- }
73
-
74
- // Add special 'random' option
75
- personalities.push({
76
- name: 'random',
77
- description: 'Picks randomly each time',
78
- current: currentPersonality === 'random'
79
- });
80
-
81
- return personalities.sort((a, b) => {
82
- // Keep 'random' at the end
83
- if (a.name === 'random') return 1;
84
- if (b.name === 'random') return -1;
85
- return a.name.localeCompare(b.name);
86
- });
87
- }
88
-
89
- /**
90
- * Main function
91
- */
92
- function main() {
93
- const args = process.argv.slice(2);
94
-
95
- // Parse arguments
96
- const personalitiesDir = args[0] || path.join(os.homedir(), '.claude', 'personalities');
97
- const currentPersonality = args[1] || 'normal';
98
-
99
- const personalities = getPersonalities(personalitiesDir, currentPersonality);
100
-
101
- // Display with boxen
102
- const output = formatPersonalitiesList(personalities, {
103
- columns: 2,
104
- showUsage: true
105
- });
106
-
107
- console.log(output);
108
- }
109
-
110
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Personality List Display - Beautiful multi-column personality listing
4
+ * Called by personality-manager.sh to display personalities with boxen formatting
5
+ */
6
+
7
+ import { formatPersonalitiesList } from '../utils/list-formatter.js';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import os from 'os';
11
+
12
+ /**
13
+ * Get personality description from markdown file
14
+ */
15
+ function getPersonalityDescription(filePath) {
16
+ try {
17
+ const content = fs.readFileSync(filePath, 'utf8');
18
+
19
+ // Try to extract description from frontmatter or first paragraph
20
+ const descMatch = content.match(/description:\s*(.+)/i);
21
+ if (descMatch) {
22
+ return descMatch[1].trim();
23
+ }
24
+
25
+ // Try to get first line after frontmatter
26
+ const lines = content.split('\n');
27
+ let inFrontmatter = false;
28
+ let frontmatterCount = 0;
29
+
30
+ for (const line of lines) {
31
+ if (line.trim() === '---') {
32
+ frontmatterCount++;
33
+ inFrontmatter = frontmatterCount === 1;
34
+ continue;
35
+ }
36
+
37
+ if (!inFrontmatter && frontmatterCount >= 2 && line.trim()) {
38
+ // First non-empty line after frontmatter
39
+ return line.trim().replace(/^#+\s*/, '').substring(0, 50);
40
+ }
41
+ }
42
+
43
+ return '';
44
+ } catch (error) {
45
+ return '';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get all personalities from directory
51
+ */
52
+ function getPersonalities(personalitiesDir, currentPersonality) {
53
+ const personalities = [];
54
+
55
+ if (!fs.existsSync(personalitiesDir)) {
56
+ return personalities;
57
+ }
58
+
59
+ const files = fs.readdirSync(personalitiesDir);
60
+ for (const file of files) {
61
+ if (file.endsWith('.md')) {
62
+ const name = path.basename(file, '.md');
63
+ const filePath = path.join(personalitiesDir, file);
64
+ const description = getPersonalityDescription(filePath);
65
+
66
+ personalities.push({
67
+ name,
68
+ description,
69
+ current: name === currentPersonality
70
+ });
71
+ }
72
+ }
73
+
74
+ // Add special 'random' option
75
+ personalities.push({
76
+ name: 'random',
77
+ description: 'Picks randomly each time',
78
+ current: currentPersonality === 'random'
79
+ });
80
+
81
+ return personalities.sort((a, b) => {
82
+ // Keep 'random' at the end
83
+ if (a.name === 'random') return 1;
84
+ if (b.name === 'random') return -1;
85
+ return a.name.localeCompare(b.name);
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Main function
91
+ */
92
+ function main() {
93
+ const args = process.argv.slice(2);
94
+
95
+ // Parse arguments
96
+ const personalitiesDir = args[0] || path.join(os.homedir(), '.claude', 'personalities');
97
+ const currentPersonality = args[1] || 'normal';
98
+
99
+ const personalities = getPersonalities(personalitiesDir, currentPersonality);
100
+
101
+ // Display with boxen
102
+ const output = formatPersonalitiesList(personalities, {
103
+ columns: 2,
104
+ showUsage: true
105
+ });
106
+
107
+ console.log(output);
108
+ }
109
+
110
+ main();