mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +00:00
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.
This commit is contained in:
parent
d1189f2be9
commit
37458e72a2
3 changed files with 150 additions and 1 deletions
|
|
@ -3608,6 +3608,14 @@ def cmd_profile(args):
|
||||||
else:
|
else:
|
||||||
print(f"Cloned config, .env, SOUL.md from {source_label}.")
|
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)
|
# Seed bundled skills (skip if --clone-all already copied them)
|
||||||
if not clone_all:
|
if not clone_all:
|
||||||
result = seed_profile_skills(profile_dir)
|
result = seed_profile_skills(profile_dir)
|
||||||
|
|
|
||||||
|
|
@ -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
|
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:
|
def _host_key() -> str:
|
||||||
"""Return the active Honcho host key, derived from the current Hermes profile."""
|
"""Return the active Honcho host key, derived from the current Hermes profile."""
|
||||||
return resolve_active_host()
|
return resolve_active_host()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
"""Tests for Honcho CLI helpers."""
|
"""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:
|
class TestResolveApiKey:
|
||||||
|
|
@ -27,3 +30,88 @@ class TestResolveApiKey:
|
||||||
assert _resolve_api_key({}) == "env-key"
|
assert _resolve_api_key({}) == "env-key"
|
||||||
monkeypatch.delenv("HONCHO_API_KEY", raising=False)
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue