agentvibes 5.13.0 → 5.14.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 (82) 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/kokoro-server.py +219 -219
  16. package/.claude/hooks-windows/kokoro-tts.py +107 -107
  17. package/.claude/hooks-windows/play-tts.ps1 +17 -0
  18. package/.claude/hooks-windows/soprano-gradio-synth.py +153 -153
  19. package/.claude/verbosity.txt +1 -1
  20. package/.clawdbot/README.md +105 -105
  21. package/CLAUDE.md +84 -176
  22. package/README.md +27 -2
  23. package/RELEASE_NOTES.md +86 -0
  24. package/WINDOWS-SETUP.md +208 -208
  25. package/bin/agent-vibes +39 -39
  26. package/bin/mcp-server.js +121 -121
  27. package/bin/test-bmad-pr +78 -78
  28. package/mcp-server/QUICK_START.md +203 -203
  29. package/mcp-server/README.md +345 -345
  30. package/mcp-server/examples/claude_desktop_config.json +11 -11
  31. package/mcp-server/examples/claude_desktop_config_piper.json +9 -9
  32. package/mcp-server/examples/custom_instructions.md +169 -169
  33. package/mcp-server/install-deps.js +12 -2
  34. package/mcp-server/server.py +2111 -2085
  35. package/mcp-server/test_mcp_correctness.py +504 -504
  36. package/mcp-server/test_windows_script_parity.py +339 -339
  37. package/package.json +7 -4
  38. package/setup-ssh-receiver.ps1 +259 -0
  39. package/setup-windows.ps1 +39 -29
  40. package/src/cli/list-personalities.js +110 -110
  41. package/src/commands/bmad-voices.js +394 -394
  42. package/src/console/brand-colors.js +13 -13
  43. package/src/console/constants/personalities.js +44 -44
  44. package/src/console/tabs/agents-tab.js +14 -4
  45. package/src/console/tabs/help-tab.js +314 -314
  46. package/src/console/tabs/readme-tab.js +272 -272
  47. package/src/console/tabs/settings-tab.js +31 -6
  48. package/src/console/tabs/setup-tab.js +154 -32
  49. package/src/console/tabs/voices-tab.js +48 -6
  50. package/src/console/widgets/destroy-list.js +25 -25
  51. package/src/console/widgets/notice.js +55 -55
  52. package/src/installer/language-screen.js +31 -31
  53. package/src/installer/music-file-input.js +304 -304
  54. package/src/installer.js +103 -43
  55. package/src/services/language-service.js +47 -47
  56. package/src/services/llm-provider-service.js +4 -1
  57. package/src/services/provider-voice-catalog.js +9 -5
  58. package/src/utils/audio-format-validator.js +277 -277
  59. package/src/utils/dependency-checker.js +469 -469
  60. package/src/utils/file-ownership-verifier.js +358 -358
  61. package/src/utils/music-file-validator.js +285 -285
  62. package/src/utils/preview-list-prompt.js +144 -144
  63. package/src/utils/secure-music-storage.js +412 -412
  64. package/templates/agentvibes-receiver.ps1 +311 -0
  65. package/templates/agentvibes-receiver.sh +297 -297
  66. package/voice-assignments.json +8244 -8244
  67. package/.agentvibes/config.json +0 -17
  68. package/.agentvibes/install-manifest.json +0 -342
  69. package/.claude/config/audio-effects.cfg +0 -7
  70. package/.claude/config/audio-effects.cfg.bak-kokoro +0 -7
  71. package/.claude/config/background-music-enabled.txt +0 -1
  72. package/.claude/config/background-music-position.txt +0 -27
  73. package/.claude/config/background-music-volume.txt +0 -1
  74. package/.claude/config/background-music.cfg +0 -1
  75. package/.claude/config/background-music.txt +0 -1
  76. package/.claude/config/language.txt +0 -1
  77. package/.claude/config/personality.txt +0 -1
  78. package/.claude/config/reverb-level.txt +0 -1
  79. package/.claude/config/tts-speech-rate.txt +0 -1
  80. package/.claude/config/tts-verbosity.txt +0 -1
  81. package/.claude/hooks-windows/audio-cache-utils.ps1.user.bak +0 -119
  82. package/.claude/hooks-windows/soprano-gradio-synth.py.user.bak +0 -153
@@ -1 +1 @@
1
- 20260716
1
+ 20260719
@@ -1,219 +1,219 @@
1
- #!/usr/bin/env python3
2
- #
3
- # File: .claude/hooks/kokoro-server.py
4
- #
5
- # AgentVibes - Kokoro TTS persistent daemon
6
- # Loads the Kokoro model ONCE (on GPU when available) and serves synthesis over
7
- # a localhost HTTP socket, so each request skips the ~10s model-load + CUDA-init
8
- # cost that a fresh `python kokoro-tts.py` invocation pays every time.
9
- #
10
- # This is the difference between ~31s/message (cold process per call) and
11
- # ~2-3s/message (warm resident model).
12
- #
13
- # Endpoints (bound to 127.0.0.1 only — never exposed off-host):
14
- # GET /health -> 200 {"ok":true} once the model is loaded and warmed
15
- # POST /synth -> body {text, voice, speed, output}; writes a WAV to `output`
16
- # and returns {"ok":true,"path":output} or {"ok":false,"error":...}
17
- #
18
- # Usage: python kokoro-server.py [port] (default port 7855)
19
- #
20
- # Licensed under the Apache License, Version 2.0
21
- #
22
- import json
23
- import os
24
- import sys
25
- import tempfile
26
- import threading
27
- from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
-
29
- # A bad argv[1] (non-numeric) must not crash the daemon before it can bind;
30
- # fall back to the standard kokoro-server port.
31
- try:
32
- PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 7855
33
- except (ValueError, IndexError):
34
- PORT = 7855
35
-
36
- # Language code from the voice prefix (af/am=American, bf/bm=British, etc.)
37
- LANG_MAP = {
38
- 'af': 'a', 'am': 'a', 'bf': 'b', 'bm': 'b', 'jf': 'j', 'jm': 'j',
39
- 'kf': 'k', 'km': 'k', 'zf': 'z', 'zm': 'z', 'ff': 'f', 'fm': 'f',
40
- 'hf': 'h', 'hm': 'h', 'if': 'i', 'im': 'i', 'pf': 'p', 'pm': 'p',
41
- 'ef': 'e', 'em': 'e', 'nf': 'n', 'nm': 'n',
42
- }
43
-
44
- # Heavy imports up front so the model is resident before we accept connections.
45
- import numpy as np
46
- import soundfile as sf
47
- from kokoro import KPipeline
48
-
49
- _pipelines = {}
50
- _lock = threading.Lock() # guards pipeline creation only
51
- _infer_lock = threading.Lock() # serializes inference (torch is not thread-safe)
52
-
53
-
54
- def get_pipeline(lang_code):
55
- """Return a cached KPipeline for the language, creating it once on demand."""
56
- with _lock:
57
- pipe = _pipelines.get(lang_code)
58
- if pipe is None:
59
- pipe = KPipeline(lang_code=lang_code)
60
- _pipelines[lang_code] = pipe
61
- return pipe
62
-
63
-
64
- def synth(text, voice, speed, output):
65
- """Synthesize `text` with `voice` to the `output` WAV path. Returns the path."""
66
- prefix = voice[:2].lower() if len(voice) >= 2 else 'af'
67
- lang_code = LANG_MAP.get(prefix, 'a')
68
- pipeline = get_pipeline(lang_code)
69
-
70
- chunks = []
71
- # The daemon shares one KPipeline across ThreadingHTTPServer worker threads
72
- # and torch inference is not thread-safe — run one inference at a time. This
73
- # lock is acquired after get_pipeline() has released _lock, so no deadlock.
74
- with _infer_lock:
75
- for result in pipeline(text, voice=voice, speed=speed):
76
- audio = getattr(result, "audio", None)
77
- if audio is None:
78
- try:
79
- audio = result[-1]
80
- except (TypeError, IndexError, KeyError):
81
- audio = None
82
- if audio is None:
83
- continue
84
- if hasattr(audio, "detach"):
85
- audio = audio.detach().cpu().numpy()
86
- audio = np.asarray(audio).reshape(-1)
87
- if audio.size > 0:
88
- chunks.append(audio)
89
-
90
- if not chunks:
91
- raise RuntimeError("Kokoro returned no audio samples")
92
-
93
- sf.write(output, np.concatenate(chunks), 24000) # kokoro is always 24 kHz
94
- return output
95
-
96
-
97
- def _allowed_output_dirs():
98
- """Directories a /synth request is permitted to write a WAV into.
99
-
100
- Mirrors the shell hooks' audio-cache resolution (project-local first, then
101
- the global ~/.claude/audio) plus TEMP, which is where the daemon writes its
102
- own transient/warmup files.
103
- """
104
- dirs = []
105
- proj = os.environ.get("CLAUDE_PROJECT_DIR")
106
- if proj:
107
- dirs.append(os.path.join(proj, ".claude", "audio"))
108
- home = os.environ.get("HOME") or os.path.expanduser("~")
109
- dirs.append(os.path.join(home, ".claude", "audio"))
110
- dirs.append(os.environ.get("TEMP", "/tmp"))
111
- return dirs
112
-
113
-
114
- def is_output_within_cache(output):
115
- """True if `output` resolves inside an allowed audio dir (blocks traversal).
116
-
117
- The daemon is shared across projects on a fixed port, so it cannot rely on
118
- its own CLAUDE_PROJECT_DIR to know every project's audio dir. Accept the
119
- home/TEMP locations plus any file that sits directly inside a ".claude/audio"
120
- directory (the only place the providers ever write). realpath has already
121
- collapsed any "..", so true traversal (e.g. /etc/x.wav) is still rejected.
122
- """
123
- tgt = os.path.realpath(output)
124
- for base in _allowed_output_dirs():
125
- base = os.path.realpath(base)
126
- if tgt == base or tgt.startswith(base + os.sep):
127
- return True
128
- # Project-local audio dir for ANY project — the file must live directly in
129
- # a ".claude/audio" directory.
130
- parent = os.path.dirname(tgt).replace("\\", "/").lower()
131
- if parent.endswith("/.claude/audio"):
132
- return True
133
- return False
134
-
135
-
136
- class Handler(BaseHTTPRequestHandler):
137
- # Silence default per-request stderr logging
138
- def log_message(self, *args):
139
- pass
140
-
141
- def _send(self, code, obj):
142
- data = json.dumps(obj).encode("utf-8")
143
- self.send_response(code)
144
- self.send_header("Content-Type", "application/json")
145
- self.send_header("Content-Length", str(len(data)))
146
- self.end_headers()
147
- try:
148
- self.wfile.write(data)
149
- except OSError:
150
- # Client closed the socket early (health probes do this) — WinError
151
- # 10053/10054 etc. Nothing to send to; ignore.
152
- pass
153
-
154
- def do_GET(self):
155
- if self.path == "/health":
156
- self._send(200, {"ok": True})
157
- else:
158
- self._send(404, {"ok": False, "error": "not found"})
159
-
160
- def do_POST(self):
161
- if self.path != "/synth":
162
- self._send(404, {"ok": False, "error": "not found"})
163
- return
164
- try:
165
- length = int(self.headers.get("Content-Length", 0))
166
- body = json.loads(self.rfile.read(length) or b"{}")
167
- text = body["text"]
168
- voice = body.get("voice", "af_heart")
169
- speed = float(body.get("speed", 1.0))
170
- output = body["output"]
171
- # Output must be an absolute .wav path (the provider always supplies one)
172
- if not isinstance(output, str) or not output.lower().endswith(".wav"):
173
- raise ValueError("output must be a .wav path")
174
- # Path-traversal guard: the resolved target must stay inside an
175
- # allowed audio directory — reject before any file is written.
176
- if not is_output_within_cache(output):
177
- self._send(400, {"ok": False, "error": "output path outside allowed audio directory"})
178
- return
179
- synth(text, voice, speed, output)
180
- self._send(200, {"ok": True, "path": output})
181
- except Exception as e: # noqa: BLE001 - report any failure to the client
182
- self._send(500, {"ok": False, "error": str(e)})
183
-
184
-
185
- def main():
186
- try:
187
- import torch
188
- device = "cuda" if torch.cuda.is_available() else "cpu"
189
- except Exception:
190
- device = "cpu"
191
-
192
- # Bind first so a second accidental launch fails fast (address in use) and
193
- # exits instead of loading a second copy of the model.
194
- try:
195
- server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
196
- except OSError as e:
197
- print(f"kokoro-server: port {PORT} unavailable ({e}); another instance is likely running", file=sys.stderr)
198
- sys.exit(0)
199
-
200
- # Warm the default English pipeline + trigger CUDA kernel compilation so the
201
- # very first real /synth request is already fast.
202
- warm_fd, warm_out = tempfile.mkstemp(suffix=".wav")
203
- os.close(warm_fd) # synth/soundfile reopens by path; we only need the name
204
- try:
205
- synth("ready", "af_heart", 1.0, warm_out)
206
- except Exception as e: # noqa: BLE001
207
- print(f"kokoro-server: warmup failed (continuing): {e}", file=sys.stderr)
208
- finally:
209
- try:
210
- os.remove(warm_out)
211
- except OSError:
212
- pass
213
-
214
- print(f"kokoro-server ready on 127.0.0.1:{PORT} device={device}", flush=True)
215
- server.serve_forever()
216
-
217
-
218
- if __name__ == "__main__":
219
- main()
1
+ #!/usr/bin/env python3
2
+ #
3
+ # File: .claude/hooks/kokoro-server.py
4
+ #
5
+ # AgentVibes - Kokoro TTS persistent daemon
6
+ # Loads the Kokoro model ONCE (on GPU when available) and serves synthesis over
7
+ # a localhost HTTP socket, so each request skips the ~10s model-load + CUDA-init
8
+ # cost that a fresh `python kokoro-tts.py` invocation pays every time.
9
+ #
10
+ # This is the difference between ~31s/message (cold process per call) and
11
+ # ~2-3s/message (warm resident model).
12
+ #
13
+ # Endpoints (bound to 127.0.0.1 only — never exposed off-host):
14
+ # GET /health -> 200 {"ok":true} once the model is loaded and warmed
15
+ # POST /synth -> body {text, voice, speed, output}; writes a WAV to `output`
16
+ # and returns {"ok":true,"path":output} or {"ok":false,"error":...}
17
+ #
18
+ # Usage: python kokoro-server.py [port] (default port 7855)
19
+ #
20
+ # Licensed under the Apache License, Version 2.0
21
+ #
22
+ import json
23
+ import os
24
+ import sys
25
+ import tempfile
26
+ import threading
27
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
28
+
29
+ # A bad argv[1] (non-numeric) must not crash the daemon before it can bind;
30
+ # fall back to the standard kokoro-server port.
31
+ try:
32
+ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 7855
33
+ except (ValueError, IndexError):
34
+ PORT = 7855
35
+
36
+ # Language code from the voice prefix (af/am=American, bf/bm=British, etc.)
37
+ LANG_MAP = {
38
+ 'af': 'a', 'am': 'a', 'bf': 'b', 'bm': 'b', 'jf': 'j', 'jm': 'j',
39
+ 'kf': 'k', 'km': 'k', 'zf': 'z', 'zm': 'z', 'ff': 'f', 'fm': 'f',
40
+ 'hf': 'h', 'hm': 'h', 'if': 'i', 'im': 'i', 'pf': 'p', 'pm': 'p',
41
+ 'ef': 'e', 'em': 'e', 'nf': 'n', 'nm': 'n',
42
+ }
43
+
44
+ # Heavy imports up front so the model is resident before we accept connections.
45
+ import numpy as np
46
+ import soundfile as sf
47
+ from kokoro import KPipeline
48
+
49
+ _pipelines = {}
50
+ _lock = threading.Lock() # guards pipeline creation only
51
+ _infer_lock = threading.Lock() # serializes inference (torch is not thread-safe)
52
+
53
+
54
+ def get_pipeline(lang_code):
55
+ """Return a cached KPipeline for the language, creating it once on demand."""
56
+ with _lock:
57
+ pipe = _pipelines.get(lang_code)
58
+ if pipe is None:
59
+ pipe = KPipeline(lang_code=lang_code)
60
+ _pipelines[lang_code] = pipe
61
+ return pipe
62
+
63
+
64
+ def synth(text, voice, speed, output):
65
+ """Synthesize `text` with `voice` to the `output` WAV path. Returns the path."""
66
+ prefix = voice[:2].lower() if len(voice) >= 2 else 'af'
67
+ lang_code = LANG_MAP.get(prefix, 'a')
68
+ pipeline = get_pipeline(lang_code)
69
+
70
+ chunks = []
71
+ # The daemon shares one KPipeline across ThreadingHTTPServer worker threads
72
+ # and torch inference is not thread-safe — run one inference at a time. This
73
+ # lock is acquired after get_pipeline() has released _lock, so no deadlock.
74
+ with _infer_lock:
75
+ for result in pipeline(text, voice=voice, speed=speed):
76
+ audio = getattr(result, "audio", None)
77
+ if audio is None:
78
+ try:
79
+ audio = result[-1]
80
+ except (TypeError, IndexError, KeyError):
81
+ audio = None
82
+ if audio is None:
83
+ continue
84
+ if hasattr(audio, "detach"):
85
+ audio = audio.detach().cpu().numpy()
86
+ audio = np.asarray(audio).reshape(-1)
87
+ if audio.size > 0:
88
+ chunks.append(audio)
89
+
90
+ if not chunks:
91
+ raise RuntimeError("Kokoro returned no audio samples")
92
+
93
+ sf.write(output, np.concatenate(chunks), 24000) # kokoro is always 24 kHz
94
+ return output
95
+
96
+
97
+ def _allowed_output_dirs():
98
+ """Directories a /synth request is permitted to write a WAV into.
99
+
100
+ Mirrors the shell hooks' audio-cache resolution (project-local first, then
101
+ the global ~/.claude/audio) plus TEMP, which is where the daemon writes its
102
+ own transient/warmup files.
103
+ """
104
+ dirs = []
105
+ proj = os.environ.get("CLAUDE_PROJECT_DIR")
106
+ if proj:
107
+ dirs.append(os.path.join(proj, ".claude", "audio"))
108
+ home = os.environ.get("HOME") or os.path.expanduser("~")
109
+ dirs.append(os.path.join(home, ".claude", "audio"))
110
+ dirs.append(os.environ.get("TEMP", "/tmp"))
111
+ return dirs
112
+
113
+
114
+ def is_output_within_cache(output):
115
+ """True if `output` resolves inside an allowed audio dir (blocks traversal).
116
+
117
+ The daemon is shared across projects on a fixed port, so it cannot rely on
118
+ its own CLAUDE_PROJECT_DIR to know every project's audio dir. Accept the
119
+ home/TEMP locations plus any file that sits directly inside a ".claude/audio"
120
+ directory (the only place the providers ever write). realpath has already
121
+ collapsed any "..", so true traversal (e.g. /etc/x.wav) is still rejected.
122
+ """
123
+ tgt = os.path.realpath(output)
124
+ for base in _allowed_output_dirs():
125
+ base = os.path.realpath(base)
126
+ if tgt == base or tgt.startswith(base + os.sep):
127
+ return True
128
+ # Project-local audio dir for ANY project — the file must live directly in
129
+ # a ".claude/audio" directory.
130
+ parent = os.path.dirname(tgt).replace("\\", "/").lower()
131
+ if parent.endswith("/.claude/audio"):
132
+ return True
133
+ return False
134
+
135
+
136
+ class Handler(BaseHTTPRequestHandler):
137
+ # Silence default per-request stderr logging
138
+ def log_message(self, *args):
139
+ pass
140
+
141
+ def _send(self, code, obj):
142
+ data = json.dumps(obj).encode("utf-8")
143
+ self.send_response(code)
144
+ self.send_header("Content-Type", "application/json")
145
+ self.send_header("Content-Length", str(len(data)))
146
+ self.end_headers()
147
+ try:
148
+ self.wfile.write(data)
149
+ except OSError:
150
+ # Client closed the socket early (health probes do this) — WinError
151
+ # 10053/10054 etc. Nothing to send to; ignore.
152
+ pass
153
+
154
+ def do_GET(self):
155
+ if self.path == "/health":
156
+ self._send(200, {"ok": True})
157
+ else:
158
+ self._send(404, {"ok": False, "error": "not found"})
159
+
160
+ def do_POST(self):
161
+ if self.path != "/synth":
162
+ self._send(404, {"ok": False, "error": "not found"})
163
+ return
164
+ try:
165
+ length = int(self.headers.get("Content-Length", 0))
166
+ body = json.loads(self.rfile.read(length) or b"{}")
167
+ text = body["text"]
168
+ voice = body.get("voice", "af_heart")
169
+ speed = float(body.get("speed", 1.0))
170
+ output = body["output"]
171
+ # Output must be an absolute .wav path (the provider always supplies one)
172
+ if not isinstance(output, str) or not output.lower().endswith(".wav"):
173
+ raise ValueError("output must be a .wav path")
174
+ # Path-traversal guard: the resolved target must stay inside an
175
+ # allowed audio directory — reject before any file is written.
176
+ if not is_output_within_cache(output):
177
+ self._send(400, {"ok": False, "error": "output path outside allowed audio directory"})
178
+ return
179
+ synth(text, voice, speed, output)
180
+ self._send(200, {"ok": True, "path": output})
181
+ except Exception as e: # noqa: BLE001 - report any failure to the client
182
+ self._send(500, {"ok": False, "error": str(e)})
183
+
184
+
185
+ def main():
186
+ try:
187
+ import torch
188
+ device = "cuda" if torch.cuda.is_available() else "cpu"
189
+ except Exception:
190
+ device = "cpu"
191
+
192
+ # Bind first so a second accidental launch fails fast (address in use) and
193
+ # exits instead of loading a second copy of the model.
194
+ try:
195
+ server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
196
+ except OSError as e:
197
+ print(f"kokoro-server: port {PORT} unavailable ({e}); another instance is likely running", file=sys.stderr)
198
+ sys.exit(0)
199
+
200
+ # Warm the default English pipeline + trigger CUDA kernel compilation so the
201
+ # very first real /synth request is already fast.
202
+ warm_fd, warm_out = tempfile.mkstemp(suffix=".wav")
203
+ os.close(warm_fd) # synth/soundfile reopens by path; we only need the name
204
+ try:
205
+ synth("ready", "af_heart", 1.0, warm_out)
206
+ except Exception as e: # noqa: BLE001
207
+ print(f"kokoro-server: warmup failed (continuing): {e}", file=sys.stderr)
208
+ finally:
209
+ try:
210
+ os.remove(warm_out)
211
+ except OSError:
212
+ pass
213
+
214
+ print(f"kokoro-server ready on 127.0.0.1:{PORT} device={device}", flush=True)
215
+ server.serve_forever()
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()