diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 4645318aa2e3..0c02e537b4a2 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3241,6 +3241,14 @@ DEFAULT_CONFIG = { "profile_build": "ask", }, + # Privacy-safe aggregate metrics written only to this profile's local + # telemetry directory. Collection is opt-in and no remote sink exists. + "telemetry": { + "shared_metrics": { + "enabled": False, + }, + }, + # ``hermes update`` behaviour. "updates": { # Pre-update safety backup — ONE consolidated mechanism, three modes: diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 0d1a154811b4..1148e2953647 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2199,6 +2199,37 @@ def setup_tools(config: dict, first_install: bool = False): tools_command(first_install=first_install, config=config) +# ============================================================================= +# Shared Metrics +# ============================================================================= + + +def setup_telemetry(config: dict): + """Configure the local, privacy-safe shared-metrics subscriber.""" + print_header("Shared Metrics") + print_info("Shared metrics contain only bounded counters and histograms.") + print_info("Packages stay under this Hermes profile and are not uploaded.") + + telemetry = config.get("telemetry") + if not isinstance(telemetry, dict): + telemetry = {} + config["telemetry"] = telemetry + shared_metrics = telemetry.get("shared_metrics") + if not isinstance(shared_metrics, dict): + shared_metrics = {} + telemetry["shared_metrics"] = shared_metrics + + current = shared_metrics.get("enabled") is True + shared_metrics["enabled"] = prompt_yes_no( + "Enable local shared metrics?", + default=current, + ) + if shared_metrics["enabled"]: + print_success("Local shared metrics enabled.") + else: + print_info("Local shared metrics disabled.") + + # ============================================================================= # Post-Migration Section Skip Logic # ============================================================================= @@ -2607,6 +2638,7 @@ SETUP_SECTIONS = [ ("terminal", "Terminal Backend", setup_terminal_backend), ("gateway", "Messaging Platforms (Gateway)", setup_gateway), ("tools", "Tools", setup_tools), + ("telemetry", "Shared Metrics", setup_telemetry), ("agent", "Agent Settings", setup_agent_settings), ] @@ -2705,6 +2737,7 @@ def run_setup_wizard(args): hermes setup terminal — just terminal backend hermes setup gateway — just messaging platforms hermes setup tools — just tool configuration + hermes setup telemetry — just local shared metrics hermes setup agent — just agent settings """ from hermes_cli.config import is_managed, managed_error diff --git a/hermes_cli/subcommands/setup.py b/hermes_cli/subcommands/setup.py index 406710a68875..fa0c0dc37c74 100644 --- a/hermes_cli/subcommands/setup.py +++ b/hermes_cli/subcommands/setup.py @@ -18,12 +18,21 @@ def build_setup_parser(subparsers, *, cmd_setup: Callable) -> None: "setup", help="Interactive setup wizard", description="Configure Hermes Agent with an interactive wizard. " - "Run a specific section: hermes setup model|tts|terminal|gateway|tools|agent", + "Run a specific section: " + "hermes setup model|tts|terminal|gateway|tools|telemetry|agent", ) setup_parser.add_argument( "section", nargs="?", - choices=["model", "tts", "terminal", "gateway", "tools", "agent"], + choices=[ + "model", + "tts", + "terminal", + "gateway", + "tools", + "telemetry", + "agent", + ], default=None, help="Run a specific setup section instead of the full wizard", ) diff --git a/tests/hermes_cli/test_setup_telemetry.py b/tests/hermes_cli/test_setup_telemetry.py new file mode 100644 index 000000000000..e6ebcb428c45 --- /dev/null +++ b/tests/hermes_cli/test_setup_telemetry.py @@ -0,0 +1,37 @@ +"""Tests for shared-metrics configuration discovery and setup.""" + +from __future__ import annotations + +import argparse + +from hermes_cli.config import DEFAULT_CONFIG +from hermes_cli.setup import setup_telemetry +from hermes_cli.subcommands.setup import build_setup_parser + + +def test_shared_metrics_are_registered_disabled_by_default(): + assert DEFAULT_CONFIG["telemetry"]["shared_metrics"]["enabled"] is False + + +def test_setup_telemetry_enables_shared_metrics(monkeypatch): + config = {} + monkeypatch.setattr( + "hermes_cli.setup.prompt_yes_no", + lambda _question, default: not default, + ) + + setup_telemetry(config) + + assert config["telemetry"]["shared_metrics"]["enabled"] is True + + +def test_setup_parser_accepts_telemetry_section(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command") + handler = object() + build_setup_parser(subparsers, cmd_setup=handler) + + args = parser.parse_args(["setup", "telemetry"]) + + assert args.section == "telemetry" + assert args.func is handler