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
@@ -1 +1 @@
1
- 20260716
1
+ 20260720
@@ -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()