voicesmith-mcp 1.0.12 → 1.0.14

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/bin/utils.js CHANGED
@@ -345,7 +345,7 @@ You have access to voice tools via the VoiceSmith MCP server.
345
345
  ## Speaking
346
346
  - **Opening** — Only speak at the start when you have something meaningful to say (e.g., clarifying your approach, flagging an issue). Do NOT speak filler acknowledgments like "Let me look into that." Use \`block: false\` when you do speak an opening.
347
347
  - **Closing** — Always speak a summary when done. Use \`block: true\`. Never skip the closing.
348
- - **Questions requiring user input → use \`speak_then_listen\` as your closing.** If the user literally cannot continue without providing input (e.g., choosing between options, confirming a destructive action, providing missing info), use \`speak_then_listen\`. If you can reasonably continue without their answer, use regular \`speak\`.
348
+ - **Questions → use \`speak_then_listen\`.** If your closing statement ends with a question directed at the user (ends with \`?\`), use \`speak_then_listen\` not regular \`speak\`. The only exceptions are rhetorical wrap-ups like "Standing by." or "What's next?" where you don't actually need an answer.
349
349
  - Keep spoken output brief — prefer 1-2 sentences, never exceed 3. Write details, speak summaries. No code or paths aloud.
350
350
 
351
351
  ## Speed Preferences
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voicesmith-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Local AI voice for coding assistants — TTS & STT via MCP. Kokoro ONNX + faster-whisper, fully offline.",
5
5
  "bin": {
6
6
  "voicesmith-mcp": "bin/cli.js"
package/server.py CHANGED
@@ -41,7 +41,7 @@ from shared import (
41
41
  get_logger,
42
42
  )
43
43
  from config import load_config, save_config, get_config_path, AppConfig
44
- from session_registry import register_session, unregister_session
44
+ from session_registry import register_session, rename_session, unregister_session
45
45
 
46
46
  logger = get_logger("server")
47
47
 
@@ -565,6 +565,10 @@ async def get_voice_registry() -> dict:
565
565
  async def set_voice(name: str, voice: str) -> dict:
566
566
  """Assign or reassign a voice to an agent name.
567
567
 
568
+ Also renames the session so name and voice always match.
569
+ The name is derived from the voice ID (e.g., "am_fenrir" -> "Fenrir").
570
+ If the derived name is taken by another session, returns name_occupied error.
571
+
568
572
  Args:
569
573
  name: Agent name to assign.
570
574
  voice: Kokoro voice ID (e.g., "am_eric"). Must be valid.
@@ -579,17 +583,41 @@ async def set_voice(name: str, voice: str) -> dict:
579
583
  "message": f"Voice '{voice}' not found. Use list_voices to see available options.",
580
584
  }
581
585
 
582
- _registry.set_voice(name, voice)
586
+ # Derive canonical name from voice ID (e.g., "am_fenrir" -> "Fenrir")
587
+ # The voice ID format is {prefix}_{name}, so split on underscore and capitalize
588
+ parts = voice.split("_", 1)
589
+ new_name = parts[1].capitalize() if len(parts) == 2 else name
590
+
591
+ old_name = _session_info["name"] if _session_info else name
592
+
593
+ # Update sessions.json with conflict check
594
+ if _session_info:
595
+ try:
596
+ updated = rename_session(os.getpid(), new_name, voice)
597
+ if updated:
598
+ _session_info.update(updated)
599
+ except ValueError:
600
+ return {
601
+ "success": False,
602
+ "error": "name_occupied",
603
+ "message": f"'{new_name}' is occupied by another session.",
604
+ }
605
+
606
+ # Update voice registry (remove old entry, add new)
607
+ _registry.rename_voice(old_name, new_name, voice)
583
608
 
584
609
  # Persist last voice name so it survives session restart / resume
585
610
  if _config is not None:
586
- _config.last_voice_name = name
611
+ _config.last_voice_name = new_name
587
612
  try:
588
613
  save_config(_config)
589
614
  except Exception as e:
590
615
  logger.warning(f"Failed to persist last_voice_name: {e}")
591
616
 
592
- return {"success": True, "name": name, "voice": voice}
617
+ result = {"success": True, "name": new_name, "voice": voice}
618
+ if old_name != new_name:
619
+ result["previous_name"] = old_name
620
+ return result
593
621
 
594
622
 
595
623
  @mcp.tool()
@@ -258,6 +258,52 @@ def register_session(
258
258
  return session
259
259
 
260
260
 
261
+ def rename_session(pid: int, new_name: str, new_voice: str) -> Optional[dict]:
262
+ """Rename this server's session in the registry.
263
+
264
+ Updates the name and voice fields for the entry matching pid.
265
+ Returns the updated session dict, or None if PID not found.
266
+ Raises ValueError if new_name is taken by another active session.
267
+ """
268
+ path = _sessions_path()
269
+ if not path.exists():
270
+ return None
271
+
272
+ try:
273
+ with open(path, "r+") as f:
274
+ fcntl.flock(f, fcntl.LOCK_EX)
275
+ sessions = _read_sessions(path)
276
+ sessions = _clean_stale(sessions)
277
+
278
+ # Find our entry
279
+ our_entry = None
280
+ for s in sessions:
281
+ if s.get("pid") == pid:
282
+ our_entry = s
283
+ break
284
+
285
+ if our_entry is None:
286
+ return None
287
+
288
+ # Check if new_name is taken by another session
289
+ if new_name != our_entry["name"]:
290
+ for s in sessions:
291
+ if s.get("name") == new_name and s.get("pid") != pid:
292
+ raise ValueError(
293
+ f"'{new_name}' is occupied by another session (pid {s.get('pid')})"
294
+ )
295
+
296
+ our_entry["name"] = new_name
297
+ our_entry["voice"] = new_voice
298
+ _write_sessions(path, sessions)
299
+ return dict(our_entry)
300
+ except ValueError:
301
+ raise
302
+ except OSError as e:
303
+ logger.warning(f"Failed to rename session: {e}")
304
+ return None
305
+
306
+
261
307
  def unregister_session() -> None:
262
308
  """Remove this server's session from the registry."""
263
309
  path = _sessions_path()
@@ -17,7 +17,7 @@ You have access to voice tools via the VoiceSmith MCP server.
17
17
  ## Speaking
18
18
  - **Opening** — Only speak at the start when you have something meaningful to say (e.g., clarifying your approach, flagging an issue). Do NOT speak filler acknowledgments like "Let me look into that." Use `block: false` when you do speak an opening.
19
19
  - **Closing** — Always speak a summary when done. Use `block: true`. Never skip the closing.
20
- - **Questions requiring user input → use `speak_then_listen` as your closing.** If the user literally cannot continue without providing input (e.g., choosing between options, confirming a destructive action, providing missing info), use `speak_then_listen`. If you can reasonably continue without their answer, use regular `speak`.
20
+ - **Questions → use `speak_then_listen`.** If your closing statement ends with a question directed at the user (ends with `?`), use `speak_then_listen` not regular `speak`. The only exceptions are rhetorical wrap-ups like "Standing by." or "What's next?" where you don't actually need an answer.
21
21
  - Keep spoken output brief — prefer 1-2 sentences, never exceed 3. Write details, speak summaries. No code or paths aloud.
22
22
 
23
23
  ## Speed Preferences
package/voice_registry.py CHANGED
@@ -87,6 +87,22 @@ class VoiceRegistry:
87
87
  logger.info(f"Set voice '{voice_id}' for '{name}'")
88
88
  return True
89
89
 
90
+ def rename_voice(self, old_name: str, new_name: str, voice_id: str) -> bool:
91
+ """Rename an agent's registry entry and set a new voice.
92
+
93
+ Removes the old name entry and creates a new one.
94
+ If old_name == new_name, just updates the voice in place.
95
+ Returns True if the voice_id is valid, False otherwise.
96
+ """
97
+ if voice_id not in ALL_VOICE_IDS:
98
+ logger.warning(f"Invalid voice ID '{voice_id}' for rename '{old_name}' -> '{new_name}'")
99
+ return False
100
+ if old_name != new_name and old_name in self._registry:
101
+ del self._registry[old_name]
102
+ self._registry[new_name] = voice_id
103
+ logger.info(f"Renamed '{old_name}' -> '{new_name}' with voice '{voice_id}'")
104
+ return True
105
+
90
106
  def get_registry(self) -> dict[str, str]:
91
107
  """Return a copy of the current registry."""
92
108
  return dict(self._registry)