| """hermes memory setup|status β configure memory provider plugins. |
| |
| Auto-detects installed memory providers via the plugin system. |
| Interactive curses-based UI for provider selection, then walks through |
| the provider's config schema. Writes config to config.yaml + .env. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import getpass |
| import os |
| import sys |
| from pathlib import Path |
|
|
| from hermes_constants import get_hermes_home |
|
|
|
|
| |
| |
| |
|
|
| def _curses_select(title: str, items: list[tuple[str, str]], default: int = 0) -> int: |
| """Interactive single-select with arrow keys. |
| |
| items: list of (label, description) tuples. |
| Returns selected index, or default on escape/quit. |
| """ |
| from hermes_cli.curses_ui import curses_radiolist |
| |
| display_items = [ |
| f"{label} {desc}" if desc else label |
| for label, desc in items |
| ] |
| return curses_radiolist(title, display_items, selected=default, cancel_returns=default) |
|
|
|
|
| def _prompt(label: str, default: str | None = None, secret: bool = False) -> str: |
| """Prompt for a value with optional default and secret masking.""" |
| suffix = f" [{default}]" if default else "" |
| if secret: |
| sys.stdout.write(f" {label}{suffix}: ") |
| sys.stdout.flush() |
| if sys.stdin.isatty(): |
| val = getpass.getpass(prompt="") |
| else: |
| val = sys.stdin.readline().strip() |
| else: |
| sys.stdout.write(f" {label}{suffix}: ") |
| sys.stdout.flush() |
| val = sys.stdin.readline().strip() |
| return val or (default or "") |
|
|
|
|
| |
| |
| |
|
|
| def _install_dependencies(provider_name: str) -> None: |
| """Install pip dependencies declared in plugin.yaml.""" |
| import subprocess |
| from plugins.memory import find_provider_dir |
|
|
| plugin_dir = find_provider_dir(provider_name) |
| if not plugin_dir: |
| return |
| yaml_path = plugin_dir / "plugin.yaml" |
| if not yaml_path.exists(): |
| return |
|
|
| try: |
| import yaml |
| with open(yaml_path) as f: |
| meta = yaml.safe_load(f) or {} |
| except Exception: |
| return |
|
|
| pip_deps = meta.get("pip_dependencies", []) |
| if not pip_deps: |
| return |
|
|
| |
| _IMPORT_NAMES = { |
| "honcho-ai": "honcho", |
| "mem0ai": "mem0", |
| "hindsight-client": "hindsight_client", |
| "hindsight-all": "hindsight", |
| } |
|
|
| |
| missing = [] |
| for dep in pip_deps: |
| import_name = _IMPORT_NAMES.get(dep, dep.replace("-", "_").split("[")[0]) |
| try: |
| __import__(import_name) |
| except ImportError: |
| missing.append(dep) |
|
|
| if not missing: |
| return |
|
|
| print(f"\n Installing dependencies: {', '.join(missing)}") |
|
|
| import shutil |
| uv_path = shutil.which("uv") |
| if not uv_path: |
| print(f" β uv not found β cannot install dependencies") |
| print(f" Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh") |
| print(f" Then re-run: hermes memory setup") |
| return |
|
|
| try: |
| subprocess.run( |
| [uv_path, "pip", "install", "--python", sys.executable, "--quiet"] + missing, |
| check=True, timeout=120, |
| capture_output=True, |
| ) |
| print(f" β Installed {', '.join(missing)}") |
| except subprocess.CalledProcessError as e: |
| print(f" β Failed to install {', '.join(missing)}") |
| stderr = (e.stderr or b"").decode()[:200] |
| if stderr: |
| print(f" {stderr}") |
| print(f" Run manually: uv pip install --python {sys.executable} {' '.join(missing)}") |
| except Exception as e: |
| print(f" β Install failed: {e}") |
| print(f" Run manually: uv pip install --python {sys.executable} {' '.join(missing)}") |
|
|
| |
| ext_deps = meta.get("external_dependencies", []) |
| for dep in ext_deps: |
| dep_name = dep.get("name", "") |
| check_cmd = dep.get("check", "") |
| install_cmd = dep.get("install", "") |
| if check_cmd: |
| try: |
| subprocess.run( |
| check_cmd, shell=True, capture_output=True, timeout=5 |
| ) |
| except Exception: |
| if install_cmd: |
| print(f"\n β '{dep_name}' not found. Install with:") |
| print(f" {install_cmd}") |
|
|
|
|
| def _get_available_providers() -> list: |
| """Discover memory providers from plugins/memory/. |
| |
| Returns list of (name, description, provider_instance) tuples. |
| """ |
| try: |
| from plugins.memory import discover_memory_providers, load_memory_provider |
| raw = discover_memory_providers() |
| except Exception: |
| raw = [] |
|
|
| results = [] |
| for name, desc, available in raw: |
| try: |
| provider = load_memory_provider(name) |
| if not provider: |
| continue |
| except Exception: |
| continue |
|
|
| schema = provider.get_config_schema() if hasattr(provider, "get_config_schema") else [] |
| has_secrets = any(f.get("secret") for f in schema) |
| has_non_secrets = any(not f.get("secret") for f in schema) |
| if has_secrets and has_non_secrets: |
| setup_hint = "API key / local" |
| elif has_secrets: |
| setup_hint = "requires API key" |
| elif not schema: |
| setup_hint = "no setup needed" |
| else: |
| setup_hint = "local" |
|
|
| results.append((name, setup_hint, provider)) |
| return results |
|
|
|
|
| |
| |
| |
|
|
| def cmd_setup_provider(provider_name: str) -> None: |
| """Run memory setup for a specific provider, skipping the picker.""" |
| from hermes_cli.config import load_config, save_config |
|
|
| providers = _get_available_providers() |
| match = None |
| for name, desc, provider in providers: |
| if name == provider_name: |
| match = (name, desc, provider) |
| break |
|
|
| if not match: |
| print(f"\n Memory provider '{provider_name}' not found.") |
| print(" Run 'hermes memory setup' to see available providers.\n") |
| return |
|
|
| name, _, provider = match |
|
|
| _install_dependencies(name) |
|
|
| config = load_config() |
| if not isinstance(config.get("memory"), dict): |
| config["memory"] = {} |
|
|
| if hasattr(provider, "post_setup"): |
| hermes_home = str(get_hermes_home()) |
| provider.post_setup(hermes_home, config) |
| return |
|
|
| |
| config["memory"]["provider"] = name |
| save_config(config) |
| print(f"\n Memory provider: {name}") |
| print(f" Activation saved to config.yaml\n") |
|
|
|
|
| def cmd_setup(args) -> None: |
| """Interactive memory provider setup wizard.""" |
| from hermes_cli.config import load_config, save_config |
|
|
| providers = _get_available_providers() |
|
|
| if not providers: |
| print("\n No memory provider plugins detected.") |
| print(" Install a plugin to ~/.hermes/plugins/ and try again.\n") |
| return |
|
|
| |
| items = [] |
| for name, desc, _ in providers: |
| items.append((name, f"β {desc}")) |
| items.append(("Built-in only", "β MEMORY.md / USER.md (default)")) |
|
|
| builtin_idx = len(items) - 1 |
| selected = _curses_select("Memory provider setup", items, default=builtin_idx) |
|
|
| config = load_config() |
| if not isinstance(config.get("memory"), dict): |
| config["memory"] = {} |
|
|
| |
| if selected >= len(providers) or selected < 0: |
| config["memory"]["provider"] = "" |
| save_config(config) |
| print("\n β Memory provider: built-in only") |
| print(" Saved to config.yaml\n") |
| return |
|
|
| name, _, provider = providers[selected] |
|
|
| |
| _install_dependencies(name) |
|
|
| |
| |
| if hasattr(provider, "post_setup"): |
| hermes_home = str(get_hermes_home()) |
| provider.post_setup(hermes_home, config) |
| return |
|
|
| schema = provider.get_config_schema() if hasattr(provider, "get_config_schema") else [] |
|
|
| provider_config = config["memory"].get(name, {}) |
| if not isinstance(provider_config, dict): |
| provider_config = {} |
|
|
| env_path = get_hermes_home() / ".env" |
| env_writes = {} |
|
|
| if schema: |
| print(f"\n Configuring {name}:\n") |
|
|
| for field in schema: |
| key = field["key"] |
| desc = field.get("description", key) |
| default = field.get("default") |
| |
| default_from = field.get("default_from") |
| if default_from and isinstance(default_from, dict): |
| ref_field = default_from.get("field", "") |
| ref_map = default_from.get("map", {}) |
| ref_value = provider_config.get(ref_field, "") |
| if ref_value and ref_value in ref_map: |
| default = ref_map[ref_value] |
| is_secret = field.get("secret", False) |
| choices = field.get("choices") |
| env_var = field.get("env_var") |
| url = field.get("url") |
|
|
| |
| when = field.get("when") |
| if when and isinstance(when, dict): |
| if not all(provider_config.get(k) == v for k, v in when.items()): |
| continue |
|
|
| if choices and not is_secret: |
| |
| choice_items = [(c, "") for c in choices] |
| current = provider_config.get(key, default) |
| current_idx = 0 |
| if current and current in choices: |
| current_idx = choices.index(current) |
| sel = _curses_select(f" {desc}", choice_items, default=current_idx) |
| provider_config[key] = choices[sel] |
| elif is_secret: |
| |
| existing = os.environ.get(env_var, "") if env_var else "" |
| if existing: |
| masked = f"...{existing[-4:]}" if len(existing) > 4 else "set" |
| val = _prompt(f"{desc} (current: {masked}, blank to keep)", secret=True) |
| else: |
| hint = f" Get yours at {url}" if url else "" |
| if hint: |
| print(hint) |
| val = _prompt(desc, secret=True) |
| if val and env_var: |
| env_writes[env_var] = val |
| else: |
| |
| current = provider_config.get(key) |
| effective_default = current or default |
| val = _prompt(desc, default=str(effective_default) if effective_default else None) |
| if val: |
| provider_config[key] = val |
| |
| if env_var and env_var not in env_writes: |
| env_writes[env_var] = val |
|
|
| |
| config["memory"]["provider"] = name |
| save_config(config) |
|
|
| |
| hermes_home = str(get_hermes_home()) |
| if provider_config and hasattr(provider, "save_config"): |
| try: |
| provider.save_config(provider_config, hermes_home) |
| except Exception as e: |
| print(f" Failed to write provider config: {e}") |
|
|
| |
| if env_writes: |
| _write_env_vars(env_path, env_writes) |
|
|
| print(f"\n Memory provider: {name}") |
| print(f" Activation saved to config.yaml") |
| if provider_config: |
| print(f" Provider config saved") |
| if env_writes: |
| print(f" API keys saved to .env") |
| print(f"\n Start a new session to activate.\n") |
|
|
|
|
| def _write_env_vars(env_path: Path, env_writes: dict) -> None: |
| """Append or update env vars in .env file.""" |
| env_path.parent.mkdir(parents=True, exist_ok=True) |
|
|
| existing_lines = [] |
| if env_path.exists(): |
| existing_lines = env_path.read_text().splitlines() |
|
|
| updated_keys = set() |
| new_lines = [] |
| for line in existing_lines: |
| key_match = line.split("=", 1)[0].strip() if "=" in line else "" |
| if key_match in env_writes: |
| new_lines.append(f"{key_match}={env_writes[key_match]}") |
| updated_keys.add(key_match) |
| else: |
| new_lines.append(line) |
|
|
| for key, val in env_writes.items(): |
| if key not in updated_keys: |
| new_lines.append(f"{key}={val}") |
|
|
| env_path.write_text("\n".join(new_lines) + "\n") |
|
|
|
|
| |
| |
| |
|
|
| def cmd_status(args) -> None: |
| """Show current memory provider config.""" |
| from hermes_cli.config import load_config |
|
|
| config = load_config() |
| mem_config = config.get("memory", {}) |
| provider_name = mem_config.get("provider", "") |
|
|
| print(f"\nMemory status\n" + "β" * 40) |
| print(f" Built-in: always active") |
| print(f" Provider: {provider_name or '(none β built-in only)'}") |
|
|
| if provider_name: |
| provider_config = mem_config.get(provider_name, {}) |
| if provider_config: |
| print(f"\n {provider_name} config:") |
| for key, val in provider_config.items(): |
| print(f" {key}: {val}") |
|
|
| providers = _get_available_providers() |
| found = any(name == provider_name for name, _, _ in providers) |
| if found: |
| print(f"\n Plugin: installed β") |
| for pname, _, p in providers: |
| if pname == provider_name: |
| if p.is_available(): |
| print(f" Status: available β") |
| else: |
| print(f" Status: not available β") |
| schema = p.get_config_schema() if hasattr(p, "get_config_schema") else [] |
| |
| required_fields = [f for f in schema if f.get("env_var")] |
| if required_fields: |
| print(f" Missing:") |
| for f in required_fields: |
| env_var = f.get("env_var", "") |
| url = f.get("url", "") |
| is_set = bool(os.environ.get(env_var)) |
| mark = "β" if is_set else "β" |
| line = f" {mark} {env_var}" |
| if url and not is_set: |
| line += f" β {url}" |
| print(line) |
| break |
| else: |
| print(f"\n Plugin: NOT installed β") |
| print(f" Install the '{provider_name}' memory plugin to ~/.hermes/plugins/") |
|
|
| providers = _get_available_providers() |
| if providers: |
| print(f"\n Installed plugins:") |
| for pname, desc, _ in providers: |
| active = " β active" if pname == provider_name else "" |
| print(f" β’ {pname} ({desc}){active}") |
|
|
| print() |
|
|
|
|
| |
| |
| |
|
|
| def memory_command(args) -> None: |
| """Route memory subcommands.""" |
| sub = getattr(args, "memory_command", None) |
| if sub == "setup": |
| cmd_setup(args) |
| elif sub == "status": |
| cmd_status(args) |
| else: |
| cmd_status(args) |
|
|