From 37458e72a2223cc67593db71fc794fd0ebadc055 Mon Sep 17 00:00:00 2001 From: Erosika Date: Mon, 30 Mar 2026 14:26:26 -0400 Subject: [PATCH] feat(honcho): auto-clone config to new profiles on creation When a profile is created and Honcho is already configured on the default host, automatically creates a host block for the new profile with inherited settings (memory mode, recall mode, write frequency, peer name, etc.) and auto-derived workspace/aiPeer. Zero-friction path: hermes profile create coder -> Honcho config cloned as hermes.coder with all settings inherited. --- hermes_cli/main.py | 8 +++ honcho_integration/cli.py | 53 ++++++++++++++++ tests/honcho_integration/test_cli.py | 90 +++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 1 deletion(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 6cb22c4d5..847472ec6 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3608,6 +3608,14 @@ def cmd_profile(args): else: print(f"Cloned config, .env, SOUL.md from {source_label}.") + # Auto-clone Honcho config for the new profile + try: + from honcho_integration.cli import clone_honcho_for_profile + if clone_honcho_for_profile(name): + print(f"Honcho config cloned (host: hermes.{name})") + except Exception: + pass # Honcho not installed or not configured + # Seed bundled skills (skip if --clone-all already copied them) if not clone_all: result = seed_profile_skills(profile_dir) diff --git a/honcho_integration/cli.py b/honcho_integration/cli.py index a3856ed3a..927322309 100644 --- a/honcho_integration/cli.py +++ b/honcho_integration/cli.py @@ -14,6 +14,59 @@ from hermes_constants import get_hermes_home from honcho_integration.client import resolve_active_host, resolve_config_path, GLOBAL_CONFIG_PATH, HOST +def clone_honcho_for_profile(profile_name: str) -> bool: + """Auto-clone Honcho config for a new profile from the default host block. + + Called during profile creation. If Honcho is configured on the default + host, creates a new host block for the profile with inherited settings + and auto-derived workspace/aiPeer. + + Returns True if a host block was created, False if Honcho isn't configured. + """ + cfg = _read_config() + if not cfg: + return False + + hosts = cfg.get("hosts", {}) + default_block = hosts.get(HOST, {}) + + # No default host block and no root-level API key = Honcho not configured + has_key = bool(cfg.get("apiKey") or os.environ.get("HONCHO_API_KEY")) + if not default_block and not has_key: + return False + + new_host = f"{HOST}.{profile_name}" + if new_host in hosts: + return False # already exists + + # Clone settings from default block, override identity fields + new_block = {} + for key in ("memoryMode", "recallMode", "writeFrequency", "sessionStrategy", + "sessionPeerPrefix", "contextTokens", "dialecticReasoningLevel", + "dialecticMaxChars", "saveMessages"): + val = default_block.get(key) + if val is not None: + new_block[key] = val + + # Inherit peer name from default + peer_name = default_block.get("peerName") or cfg.get("peerName") + if peer_name: + new_block["peerName"] = peer_name + + # AI peer is profile-specific; workspace is shared so all profiles + # see the same user context, sessions, and project history. + new_block["aiPeer"] = new_host + new_block["workspace"] = default_block.get("workspace") or cfg.get("workspace") or HOST + new_block["enabled"] = default_block.get("enabled", True) + + cfg.setdefault("hosts", {})[new_host] = new_block + _write_config(cfg) + + # Eagerly create the peer in Honcho so it exists before first message + _ensure_peer_exists(new_host) + return True + + def _host_key() -> str: """Return the active Honcho host key, derived from the current Hermes profile.""" return resolve_active_host() diff --git a/tests/honcho_integration/test_cli.py b/tests/honcho_integration/test_cli.py index b5a1c9f61..6f757ac8a 100644 --- a/tests/honcho_integration/test_cli.py +++ b/tests/honcho_integration/test_cli.py @@ -1,6 +1,9 @@ """Tests for Honcho CLI helpers.""" -from honcho_integration.cli import _resolve_api_key +import json +from unittest.mock import patch + +from honcho_integration.cli import _resolve_api_key, clone_honcho_for_profile class TestResolveApiKey: @@ -27,3 +30,88 @@ class TestResolveApiKey: assert _resolve_api_key({}) == "env-key" monkeypatch.delenv("HONCHO_API_KEY", raising=False) + +class TestCloneHonchoForProfile: + def test_clones_default_settings_to_new_profile(self, tmp_path): + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({ + "apiKey": "test-key", + "hosts": { + "hermes": { + "peerName": "alice", + "memoryMode": "honcho", + "recallMode": "tools", + "writeFrequency": "turn", + "dialecticReasoningLevel": "medium", + "enabled": True, + }, + }, + })) + + with patch("honcho_integration.cli._config_path", return_value=config_file): + result = clone_honcho_for_profile("coder") + + assert result is True + + cfg = json.loads(config_file.read_text()) + new_block = cfg["hosts"]["hermes.coder"] + assert new_block["peerName"] == "alice" + assert new_block["memoryMode"] == "honcho" + assert new_block["recallMode"] == "tools" + assert new_block["writeFrequency"] == "turn" + assert new_block["aiPeer"] == "hermes.coder" + assert new_block["workspace"] == "hermes.coder" + assert new_block["enabled"] is True + + def test_skips_when_no_honcho_configured(self, tmp_path): + config_file = tmp_path / "config.json" + config_file.write_text("{}") + + with patch("honcho_integration.cli._config_path", return_value=config_file): + result = clone_honcho_for_profile("coder") + + assert result is False + + def test_skips_when_host_block_already_exists(self, tmp_path): + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({ + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice"}, + "hermes.coder": {"peerName": "existing"}, + }, + })) + + with patch("honcho_integration.cli._config_path", return_value=config_file): + result = clone_honcho_for_profile("coder") + + assert result is False + cfg = json.loads(config_file.read_text()) + assert cfg["hosts"]["hermes.coder"]["peerName"] == "existing" + + def test_inherits_peer_name_from_root_when_not_in_host(self, tmp_path): + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({ + "apiKey": "key", + "peerName": "root-alice", + "hosts": {"hermes": {}}, + })) + + with patch("honcho_integration.cli._config_path", return_value=config_file): + clone_honcho_for_profile("dreamer") + + cfg = json.loads(config_file.read_text()) + assert cfg["hosts"]["hermes.dreamer"]["peerName"] == "root-alice" + + def test_works_with_api_key_only_no_host_block(self, tmp_path): + config_file = tmp_path / "config.json" + config_file.write_text(json.dumps({"apiKey": "key"})) + + with patch("honcho_integration.cli._config_path", return_value=config_file): + result = clone_honcho_for_profile("coder") + + assert result is True + cfg = json.loads(config_file.read_text()) + assert cfg["hosts"]["hermes.coder"]["aiPeer"] == "hermes.coder" + assert cfg["hosts"]["hermes.coder"]["workspace"] == "hermes.coder" +