Preserve macOS app permissions via dynamic launcher, .env workspace path, and session/audio tools

This commit is contained in:
Adolfo Reyna
2026-08-11 21:15:37 -04:00
parent 583131221c
commit b5034b4b16
17 changed files with 1888 additions and 68 deletions
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Inspect or switch VoiceAgent's live input/output device through its local UI API."""
import argparse
import json
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = "http://127.0.0.1:8888"
def request(path, method="GET", payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = Request(BASE_URL + path, data=data, method=method)
if data is not None:
req.add_header("Content-Type", "application/json")
try:
with urlopen(req, timeout=5) as response:
return json.load(response)
except HTTPError as exc:
try:
message = json.load(exc)
except Exception:
message = {"error": exc.read().decode("utf-8", "replace")}
raise RuntimeError(message.get("error", str(exc))) from exc
except URLError as exc:
raise RuntimeError(f"VoiceAgent is not reachable at {BASE_URL}: {exc.reason}") from exc
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("list", help="List selectable input/output devices.")
select = commands.add_parser("set", help="Switch one live route or follow the macOS default.")
select.add_argument("direction", choices=("input", "output"))
select.add_argument("device", help="Unique device-name substring, PortAudio index, or 'default'.")
args = parser.parse_args()
try:
if args.command == "list":
result = request("/api/audio-devices")
for device in result["devices"]:
kinds = "/".join(kind for kind in ("input" if device["input"] else "", "output" if device["output"] else "") if kind)
print(f"[{device['id']}] {device['name']} ({kinds})")
else:
result = request("/api/audio-device", "POST", {"direction": args.direction, "device": args.device})
mode = "following macOS default" if result["following_default"] else f"pinned to [{result['device']}]"
print(f"{args.direction} changed to {result['name']} ({mode})")
return 0
except (KeyError, RuntimeError) as exc:
print(f"audio-tool: {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""CLI helper to list, get, and switch Hermes agent profiles for VoiceAgent."""
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Add VoiceAgent1 project root to sys.path
project_root = Path(__file__).resolve().parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from hermes_llm import find_hermes_cli
def _get_hermes_cli() -> str:
cli = find_hermes_cli()
if not cli:
raise RuntimeError("Hermes CLI binary not found in PATH or standard paths")
return cli
def list_profiles() -> str:
cli = _get_hermes_cli()
res = subprocess.run([cli, "profile", "list"], capture_output=True, text=True)
if res.returncode != 0:
return f"Error listing profiles: {res.stderr.strip()}"
return res.stdout.strip()
def get_current_profile() -> str:
cli = _get_hermes_cli()
res = subprocess.run([cli, "profile", "list"], capture_output=True, text=True)
if res.returncode == 0:
for line in res.stdout.splitlines():
line_clean = line.strip()
if line_clean.startswith("◆") or line_clean.startswith("*"):
parts = line_clean.lstrip("◆* ").split()
if parts:
return parts[0]
return "default"
def set_profile(profile_name: str) -> tuple[bool, str]:
target = profile_name.strip().lstrip("◆* ")
if not target:
return False, "No profile name specified."
cli = _get_hermes_cli()
res = subprocess.run([cli, "profile", "use", target], capture_output=True, text=True)
if res.returncode == 0:
output_msg = res.stdout.strip() or f"Switched to Hermes profile '{target}'."
try:
import web_server
web_server.broadcast_event("status_change", {"profile": target})
except Exception:
pass
return True, output_msg
else:
err_msg = res.stderr.strip() or res.stdout.strip() or f"Failed to set profile '{target}'."
return False, err_msg
def main():
if len(sys.argv) < 2 or sys.argv[1] in ("list", "ls", "--list"):
print(list_profiles())
return
action = sys.argv[1].lower()
if action in ("get", "current", "show"):
print(f"Active Hermes Profile: {get_current_profile()}")
return
if action in ("set", "use", "change") and len(sys.argv) >= 3:
target = sys.argv[2]
ok, msg = set_profile(target)
print(msg)
else:
# Treat single argument as target profile
target = sys.argv[1]
ok, msg = set_profile(target)
print(msg)
if __name__ == "__main__":
main()
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""CLI helper to inspect and reset Hermes voice agent sessions."""
import json
import sys
from pathlib import Path
# Add VoiceAgent1 project root to sys.path
project_root = Path(__file__).resolve().parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
SESSION_FILE_NAME = ".hermes-voice-session.json"
def get_session_file(app_dir: Path | None = None) -> Path:
base_dir = app_dir or project_root
return base_dir / SESSION_FILE_NAME
def get_active_session_id(app_dir: Path | None = None) -> str | None:
session_file = get_session_file(app_dir)
if session_file.exists():
try:
data = json.loads(session_file.read_text())
sid = data.get("session_id")
if sid and isinstance(sid, str):
return sid.strip()
except Exception:
pass
return None
def reset_session(app_dir: Path | None = None) -> tuple[bool, str]:
session_file = get_session_file(app_dir)
deleted = False
if session_file.exists():
try:
session_file.unlink()
deleted = True
except Exception as e:
return False, f"Could not remove session file: {e}"
msg = "Session reset successfully. A fresh Hermes session will start on the next turn."
if not deleted:
msg = "No active session file found. Next turn will start with a fresh session."
try:
import web_server
web_server.broadcast_event("session_reset", {"message": msg})
except Exception:
pass
return True, msg
def main():
app_dir = project_root
if len(sys.argv) < 2 or sys.argv[1] in ("get", "info", "current", "show"):
sid = get_active_session_id(app_dir)
if sid:
print(f"Active Session ID: {sid}")
else:
print("No active Hermes session (a new session will start on the next turn).")
return
action = sys.argv[1].lower()
if action in ("reset", "new", "clear"):
ok, msg = reset_session(app_dir)
print(msg)
else:
print(f"Unknown action: {sys.argv[1]}. Usage: python bin/session_tool.py [get|reset]")
sys.exit(1)
if __name__ == "__main__":
main()