Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ def _install_macos(name: str, extras=()) -> None:
env = {} if name == "tray" else _dotenv_values(project / ".env")
env["PATH"] = _unit_path_env(_find_uv()) + ":" + os.environ.get("PATH", "")

# The pendant (BLE) extra decodes Opus audio via opuslib, which loads the
# native libopus through ctypes.util.find_library("opus"). That search does
# not include Homebrew's lib dir, and launchd hands the agent a bare
# environment, so point dyld's fallback search at the Homebrew prefixes
# (Apple Silicon + Intel). Harmless when the dirs are absent.
if "pendant" in extras:
env["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib:/usr/local/lib"

plist = {
"Label": label,
"ProgramArguments": _exec_argv(name, extras),
Expand Down
21 changes: 13 additions & 8 deletions extras/local-wearable-client/backend_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import websockets
from dotenv import load_dotenv

load_dotenv()
# Unified config: read the repository-root .env shared with the tray and vault
# sync (BACKEND_URL / AUTH_USERNAME / AUTH_PASSWORD). Mirrors vault_core.
_REPO_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(_REPO_ROOT / ".env")

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -75,16 +78,17 @@ def _resolve_backend_url() -> str:
logger.info("Backend URL from BACKEND_HOST: %s", url)
return url

_repo_root = str(Path(__file__).resolve().parent.parent.parent)
if _repo_root not in sys.path:
sys.path.insert(0, _repo_root)
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
try:
# Lazy import: sys.path-dependent (repo-root discovery.py, inserted above)
from discovery import resolve_backend_url

return resolve_backend_url(None, logger=logger)
# BACKEND_URL is the unified key (same one the tray/vault sync reads);
# an explicit value wins over discovery.
return resolve_backend_url(os.getenv("BACKEND_URL"), logger=logger)
except ImportError:
logger.warning("discovery module unavailable; set BACKEND_HOST in .env")
logger.warning("discovery module unavailable; set BACKEND_URL in .env")
return "http://localhost:8000"


Expand All @@ -94,8 +98,9 @@ def _resolve_backend_url() -> str:
websocket_uri = f"{'wss' if USE_HTTPS else 'ws'}://{_host_part}/ws?codec=opus"
logger.info("Wearable backend resolved: %s (ws: %s)", backend_url, websocket_uri)

ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL")
# Unified auth keys (AUTH_*), with the legacy ADMIN_* names as fallbacks.
ADMIN_PASSWORD = os.getenv("AUTH_PASSWORD") or os.getenv("ADMIN_PASSWORD")
ADMIN_EMAIL = os.getenv("AUTH_USERNAME") or os.getenv("ADMIN_EMAIL")

# Module-level websocket reference for sending control messages (e.g., button events)
_active_websocket = None
Expand Down
24 changes: 15 additions & 9 deletions extras/local-wearable-client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import shutil
import socket
import time
from pathlib import Path
from typing import Any, Callable

import yaml
Expand Down Expand Up @@ -48,29 +49,34 @@
)
logger = logging.getLogger(__name__)

load_dotenv()
# Unified config lives in the repository-root .env (shared with the tray and
# vault sync), same as backend_sender.
_REPO_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(_REPO_ROOT / ".env")

CONFIG_PATH = os.path.join(os.path.dirname(__file__), "devices.yml")
CONFIG_TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "devices.yml.template")
ENV_PATH = os.path.join(os.path.dirname(__file__), ".env")
ENV_PATH = str(_REPO_ROOT / ".env")


def check_config() -> bool:
"""Check that required configuration is present. Returns True if backend streaming is possible."""
if not os.path.exists(ENV_PATH):
logger.warning(
"No .env file found — copy .env.template to .env and fill in your settings"
"No repository-root .env found — copy .env.template to .env and fill in your settings"
)
logger.warning("Audio will be saved locally but NOT streamed to the backend")
return False

# AUTH_*/BACKEND_URL are the unified keys; ADMIN_*/BACKEND_HOST are accepted
# as legacy fallbacks.
missing = []
if not os.getenv("ADMIN_EMAIL"):
missing.append("ADMIN_EMAIL")
if not os.getenv("ADMIN_PASSWORD"):
missing.append("ADMIN_PASSWORD")
if not os.getenv("BACKEND_HOST"):
missing.append("BACKEND_HOST")
if not (os.getenv("AUTH_USERNAME") or os.getenv("ADMIN_EMAIL")):
missing.append("AUTH_USERNAME")
if not (os.getenv("AUTH_PASSWORD") or os.getenv("ADMIN_PASSWORD")):
missing.append("AUTH_PASSWORD")
if not (os.getenv("BACKEND_URL") or os.getenv("BACKEND_HOST")):
missing.append("BACKEND_URL")

if missing:
logger.warning("Missing environment variables: %s", ", ".join(missing))
Expand Down
Loading