Files
hermes-agent/hermes_cli/memory_setup.py
ethernet e8fcb007b9 Merge remote-tracking branch 'upstream/main' into ethie/pm-clean
# Conflicts:
#	AGENTS.md
#	acp_adapter/edit_approval.py
#	acp_adapter/server.py
#	agent/agent_init.py
#	agent/anthropic_adapter.py
#	agent/anthropic_credentials.py
#	agent/auxiliary_client.py
#	agent/azure_identity_adapter.py
#	agent/bedrock_adapter.py
#	agent/browser_registry.py
#	agent/chat_completion_helpers.py
#	agent/coding_context.py
#	agent/context_references.py
#	agent/conversation_loop.py
#	agent/copilot_acp_client.py
#	agent/credits_tracker.py
#	agent/curator.py
#	agent/curator_backup.py
#	agent/deadline.py
#	agent/display.py
#	agent/errors.py
#	agent/estop.py
#	agent/i18n.py
#	agent/image_gen_registry.py
#	agent/image_routing.py
#	agent/learning_graph.py
#	agent/learning_mutations.py
#	agent/lsp/servers.py
#	agent/model_metadata.py
#	agent/models_dev.py
#	agent/monitoring/gateway_health_export.py
#	agent/monitoring/otlp_exporter.py
#	agent/pet/store.py
#	agent/process_bootstrap.py
#	agent/prompt_builder.py
#	agent/proxy_sources/iron_proxy.py
#	agent/secret_sources/_cache.py
#	agent/secret_sources/bitwarden.py
#	agent/secret_sources/registry.py
#	agent/shell_hooks.py
#	agent/skill_bundles.py
#	agent/skill_commands.py
#	agent/skill_utils.py
#	agent/ssl_guard.py
#	agent/ssl_verify.py
#	agent/system_prompt.py
#	agent/terminal_env_registry.py
#	agent/trace_upload.py
#	agent/transcription_registry.py
#	agent/tts_registry.py
#	agent/verify/environment.py
#	agent/vertex_adapter.py
#	agent/video_gen_registry.py
#	agent/web_search_registry.py
#	cli.py
#	cron/jobs.py
#	cron/scheduler.py
#	gateway/agent_cache_pressure.py
#	gateway/cgroup_cleanup.py
#	gateway/channel_directory.py
#	gateway/config.py
#	gateway/control_socket.py
#	gateway/dead_targets.py
#	gateway/drain_control.py
#	gateway/hooks.py
#	gateway/kanban_watchers.py
#	gateway/lifecycle_ledger.py
#	gateway/mirror.py
#	gateway/pairing.py
#	gateway/platform_registry.py
#	gateway/platforms/helpers.py
#	gateway/platforms/weixin.py
#	gateway/readiness.py
#	gateway/restart_loop_guard.py
#	gateway/rich_sent_store.py
#	gateway/run.py
#	gateway/session.py
#	gateway/shutdown_flush.py
#	gateway/shutdown_forensics.py
#	gateway/slash_commands.py
#	gateway/status.py
#	gateway/sticker_cache.py
#	gateway/whatsapp_identity.py
#	hermes_bootstrap.py
#	hermes_cli/_early_recovery.py
#	hermes_cli/_install_repair.py
#	hermes_cli/_startup_fast.py
#	hermes_cli/_subprocess_compat.py
#	hermes_cli/agent_plugins.py
#	hermes_cli/auth.py
#	hermes_cli/backup.py
#	hermes_cli/banner.py
#	hermes_cli/browser_connect.py
#	hermes_cli/build_info.py
#	hermes_cli/cli_agent_setup_mixin.py
#	hermes_cli/cli_commands_mixin.py
#	hermes_cli/codex_models.py
#	hermes_cli/config.py
#	hermes_cli/config_defaults.py
#	hermes_cli/config_migrations.py
#	hermes_cli/container_boot.py
#	hermes_cli/dashboard_auth/registry.py
#	hermes_cli/debug.py
#	hermes_cli/dep_ensure.py
#	hermes_cli/doctor.py
#	hermes_cli/doctor_live.py
#	hermes_cli/dump.py
#	hermes_cli/env_loader.py
#	hermes_cli/foreign_sessions.py
#	hermes_cli/gateway.py
#	hermes_cli/gateway_windows.py
#	hermes_cli/gui_uninstall.py
#	hermes_cli/image_provenance.py
#	hermes_cli/install_identity.py
#	hermes_cli/kanban.py
#	hermes_cli/kanban_db.py
#	hermes_cli/linux_desktop_entry.py
#	hermes_cli/local_runtime/binaries.py
#	hermes_cli/local_runtime/endpoint.py
#	hermes_cli/local_runtime/growth.py
#	hermes_cli/local_runtime/supervisor.py
#	hermes_cli/logs.py
#	hermes_cli/macos_tcc_anchor.py
#	hermes_cli/main.py
#	hermes_cli/memory_setup.py
#	hermes_cli/model_catalog.py
#	hermes_cli/models.py
#	hermes_cli/nous_subscription.py
#	hermes_cli/npm_engine.py
#	hermes_cli/plugin_index.py
#	hermes_cli/plugins.py
#	hermes_cli/plugins_cmd.py
#	hermes_cli/profile_distribution.py
#	hermes_cli/profiles.py
#	hermes_cli/prompt_size.py
#	hermes_cli/psutil_android.py
#	hermes_cli/runtime_repair.py
#	hermes_cli/security_advisories.py
#	hermes_cli/security_audit.py
#	hermes_cli/security_audit_startup.py
#	hermes_cli/service_manager.py
#	hermes_cli/session_export_md.py
#	hermes_cli/setup.py
#	hermes_cli/skills_hub.py
#	hermes_cli/slack_cli.py
#	hermes_cli/status.py
#	hermes_cli/subcommands/gateway.py
#	hermes_cli/subcommands/uninstall.py
#	hermes_cli/tools_config.py
#	hermes_cli/uninstall.py
#	hermes_cli/update_cmd.py
#	hermes_cli/update_contract.py
#	hermes_cli/update_inventory.py
#	hermes_cli/update_lock.py
#	hermes_cli/update_receipt.py
#	hermes_cli/urllib_security.py
#	hermes_cli/web_routers/local_models.py
#	hermes_cli/web_routers/profiles.py
#	hermes_cli/web_routers/skills.py
#	hermes_cli/web_server.py
#	hermes_constants.py
#	hermes_state.py
#	plugins/disk-cleanup/__init__.py
#	plugins/disk-cleanup/disk_cleanup.py
#	plugins/google_meet/node/registry.py
#	plugins/google_meet/node/server.py
#	plugins/google_meet/process_manager.py
#	plugins/google_meet/realtime/openai_client.py
#	plugins/hermes-achievements/dashboard/plugin_api.py
#	plugins/memory/hindsight/__init__.py
#	plugins/memory/honcho/__init__.py
#	plugins/memory/honcho/cli.py
#	plugins/memory/honcho/client.py
#	plugins/memory/honcho/oauth.py
#	plugins/memory/honcho/session.py
#	plugins/memory/mem0/__init__.py
#	plugins/memory/mem0/_setup.py
#	plugins/memory/openviking/__init__.py
#	plugins/memory/retaindb/__init__.py
#	plugins/memory/supermemory/__init__.py
#	plugins/platforms/a2a/protocol.py
#	plugins/platforms/dingtalk/adapter.py
#	plugins/platforms/discord/adapter.py
#	plugins/platforms/feishu/adapter.py
#	plugins/platforms/google_chat/adapter.py
#	plugins/platforms/matrix/adapter.py
#	plugins/platforms/photon/adapter.py
#	plugins/platforms/photon/auth.py
#	plugins/platforms/photon/cli.py
#	plugins/platforms/slack/adapter.py
#	plugins/platforms/teams/adapter.py
#	plugins/platforms/telegram/adapter.py
#	plugins/platforms/wecom/callback_adapter.py
#	plugins/platforms/whatsapp/adapter.py
#	plugins/teams_pipeline/store.py
#	plugins/video_gen/fal/__init__.py
#	plugins/web/ddgs/provider.py
#	plugins/web/exa/provider.py
#	plugins/web/firecrawl/provider.py
#	plugins/web/parallel/provider.py
#	tests/agent/test_ssl_ca_guard.py
#	tests/hermes_cli/test_certifi_repair.py
#	tests/hermes_cli/test_cmd_update.py
#	tests/hermes_cli/test_cmd_update_apt.py
#	tests/hermes_cli/test_dashboard_unified_launch.py
#	tests/hermes_cli/test_dep_ensure.py
#	tests/hermes_cli/test_doctor.py
#	tests/hermes_cli/test_doctor_live.py
#	tests/hermes_cli/test_gui_command.py
#	tests/hermes_cli/test_kanban_boards.py
#	tests/hermes_cli/test_kanban_db.py
#	tests/hermes_cli/test_lazy_refresh_venv_repair.py
#	tests/hermes_cli/test_memory_setup_provider_arg.py
#	tests/hermes_cli/test_nous_subscription.py
#	tests/hermes_cli/test_pip_install_detection.py
#	tests/hermes_cli/test_profile_export_credentials.py
#	tests/hermes_cli/test_psutil_android_extract.py
#	tests/hermes_cli/test_status.py
#	tests/hermes_cli/test_tui_npm_install.py
#	tests/hermes_cli/test_update_fleet_restart_pending.py
#	tests/hermes_cli/test_update_head_moved_gate.py
#	tests/hermes_cli/test_update_interrupted_recovery.py
#	tests/hermes_cli/test_web_server.py
#	tests/hermes_cli/test_web_ui_build.py
#	tests/test_hermes_logging.py
#	tests/test_managed_runtime_resolution.py
#	tests/tools/test_browser_chromium_autoinstall.py
#	tests/tools/test_browser_chromium_check.py
#	tests/tools/test_browser_homebrew_paths.py
#	tests/tools/test_browser_lightpanda.py
#	tests/tools/test_browser_npx_warmup.py
#	tests/tools/test_browser_open_timeout.py
#	tests/tools/test_browser_orphan_reaper.py
#	tests/tools/test_browser_real_profile.py
#	tests/tools/test_browser_suspect_recycle.py
#	tests/tools/test_find_shell.py
#	tests/tools/test_local_env_blocklist.py
#	tests/tools/test_macos_protected_search.py
#	tests/tui_gateway/test_compute_host.py
#	tools/approval.py
#	tools/blueprints.py
#	tools/bot_mode_dm.py
#	tools/bot_mode_probe.py
#	tools/bot_relay.py
#	tools/browser_tool.py
#	tools/browser_use_cli.py
#	tools/checkpoint_manager.py
#	tools/code_execution_tool.py
#	tools/code_kernel.py
#	tools/computer_use/cua_backend.py
#	tools/cronjob_tools.py
#	tools/discord_tool.py
#	tools/environments/base.py
#	tools/environments/daytona.py
#	tools/environments/local.py
#	tools/environments/modal.py
#	tools/environments/vercel_sandbox.py
#	tools/fal_common.py
#	tools/file_operations.py
#	tools/lazy_deps.py
#	tools/mcp_tool.py
#	tools/neutts_synth.py
#	tools/process_registry.py
#	tools/read_extract.py
#	tools/registry.py
#	tools/skill_ledger.py
#	tools/skill_linter.py
#	tools/skill_manager_tool.py
#	tools/skill_usage.py
#	tools/skills_ast_audit.py
#	tools/skills_guard.py
#	tools/skills_hub.py
#	tools/skills_sync.py
#	tools/skills_sync_client.py
#	tools/skills_tool.py
#	tools/terminal_scope.py
#	tools/terminal_tool.py
#	tools/tirith_security.py
#	tools/transcription_tools.py
#	tools/tts_tool.py
#	tools/vision_tools.py
#	tools/voice_mode.py
#	tools/wake_word.py
#	tools/web_result_cache.py
#	tools/website_policy.py
#	tools/working_diff.py
#	tools/write_approval.py
#	tui_gateway/entry.py
#	tui_gateway/methods_tools.py
#	tui_gateway/server.py
2026-09-04 13:03:39 -04:00

467 lines
18 KiB
Python

"""hermes memory setup|status — configure memory provider plugins."""
from __future__ import annotations
import os
import sys
import shlex
from pathlib import Path
from hermes_constants import get_hermes_home
from hermes_cli.secret_prompt import masked_secret_prompt
_CANCELLED = -1
# pip name → import name mapping for packages where they differ
_IMPORT_NAMES = {
"honcho-ai": "honcho",
"mem0ai": "mem0",
"hindsight-client": "hindsight_client",
"hindsight-all": "hindsight"}
def _provider_extras(provider_name: str, manifest: dict, plugin_dir=None) -> list[str]:
"""The pyproject extras a provider needs on THIS install.
``plugin.yaml``'s ``extra:`` names the baseline. Hindsight's
``local_embedded`` mode needs the daemon+embedder wheel on top
(hindsight-local, #70636) — mode lives in its config file, which the
manifest can't express.
A provider manifest with legacy ``pip_dependencies`` (no ``extra:``,
no pyproject of its own — the third-party plugin shape, e.g. a
directory-installed external provider) is bridged: pm materializes
the specs into a generated pyproject.toml, and the provider becomes
a workspace member installed by the venv sync. Returns [] either
way — no EXTRA to sync, the union carries the deps."""
extras = []
declared = manifest.get("extra")
if isinstance(declared, str) and declared:
extras.append(declared)
elif plugin_dir is not None and manifest.get("pip_dependencies"):
try:
from pm.workspace import materialize_legacy_pyproject
materialize_legacy_pyproject(Path(plugin_dir))
except Exception:
pass
# hindsight local_embedded needs hindsight-all, whose protobuf floor
# conflicts with mem0/modal — it cannot be a venv extra. It stays a
# provider-owned install until plugin side-venvs land (plan step 5).
return extras
def _curses_select(
title: str, items: list[tuple[str, str]], default: int = 0, *, cancel_returns: int | None = None
) -> int:
"""Interactive single-select with arrow keys."""
from hermes_cli.curses_ui import curses_radiolist
if cancel_returns is None:
cancel_returns = default
display_items = [f"{label} - {desc}" if desc else label for label, desc in items]
result = curses_radiolist(title, display_items, selected=default, cancel_returns=cancel_returns)
_clear_interactive_transition()
return result
def _print_cancelled_setup() -> None:
print("\n Cancelled. No changes saved.\n")
def _clear_interactive_transition() -> None:
"""Clear stale curses content before entering a follow-up setup screen."""
if not sys.stdout.isatty():
return
sys.stdout.write("\033[2J\033[H")
sys.stdout.flush()
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:
val = masked_secret_prompt(f" {label}{suffix}: ")
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, *, force: bool = False) -> None:
"""Install pip dependencies declared in ``plugin.yaml``.
With ``force`` every declared dependency goes to the installer even if it imports (the resolver
no-ops when nothing drifted) — how ``hermes update`` heals a provider after a venv rebuild.
When ``force`` is true, every declared dependency is handed to the installer even if its import
currently succeeds — the resolver then reinstalls anything missing or version-drifted and no-ops on
satisfied ranges. This is how ``hermes update`` heals the active memory provider after a venv
rebuild/sync removed or downgraded its bridge packages (#53272, #70636).
"""
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, encoding="utf-8-sig") as f:
meta = yaml.safe_load(f) or {}
except Exception:
return
extras = _provider_extras(provider_name, meta, plugin_dir=plugin_dir)
if not extras:
return
import pm
missing = [e for e in extras if force or not pm.available(e)]
if not missing:
return
print(f"\n Installing dependencies: {', '.join(missing)}")
try:
pm.sync_venv(missing, explicit=True)
print(f" ✓ Installed {', '.join(missing)}")
except Exception as e:
print(f" ⚠ Install failed: {e}")
print(" Run manually: hermes pm install")
# Also show external (non-pip) dependencies that are missing.
for dep in meta.get("external_dependencies", []):
check_cmd = dep.get("check", "")
install_cmd = dep.get("install", "")
if check_cmd:
try:
subprocess.run(shlex.split(check_cmd), check=True, capture_output=True, timeout=5)
except Exception:
if install_cmd:
print(f"\n ⚠ '{dep.get('name', '')}' not found. Install with:")
print(f" {install_cmd}")
def _schema_of(provider) -> list:
return provider.get_config_schema() if hasattr(provider, "get_config_schema") else []
def _get_available_providers() -> list:
"""Discover memory providers from plugins/memory/ as ``(name, setup_hint, provider)`` 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 = _schema_of(provider)
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 _find_provider(providers: list, provider_name: str):
return next((p for p in providers if p[0] == provider_name), None)
def _post_setup_hook(provider, config: dict) -> bool:
"""Normalize the ``memory`` block; True when the provider's ``post_setup`` took over (it owns
config, connection test and activation), so the caller must stop."""
if not isinstance(config.get("memory"), dict):
config["memory"] = {}
if hasattr(provider, "post_setup"):
provider.post_setup(str(get_hermes_home()), config)
return True
return False
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
match = _find_provider(_get_available_providers(), provider_name)
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
_clear_interactive_transition()
_install_dependencies(name)
config = load_config()
if _post_setup_hook(provider, config):
return
# Fallback: generic schema-based setup (same as cmd_setup)
config["memory"]["provider"] = name
save_config(config)
print(f"\n Memory provider: {name}")
print(" Activation saved to config.yaml\n")
def _prompt_schema_fields(name: str, schema: list, provider_config: dict, env_writes: dict) -> bool:
"""Walk a provider's config schema, prompting per field. False when the user cancelled."""
print(f"\n Configuring {name}:\n")
for field in schema:
key = field["key"]
desc = field.get("description", key)
default = field.get("default")
# Dynamic default: look up default from another field's value
default_from = field.get("default_from")
if default_from and isinstance(default_from, dict):
ref_value = provider_config.get(default_from.get("field", ""), "")
ref_map = default_from.get("map", {})
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) and not all(provider_config.get(k) == v for k, v in when.items()):
continue
if choices and not is_secret:
current = provider_config.get(key, default)
current_idx = choices.index(current) if current and current in choices else 0
sel = _curses_select(
f" {desc}", [(c, "") for c in choices], default=current_idx, cancel_returns=_CANCELLED
)
if sel == _CANCELLED:
_print_cancelled_setup()
return False
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:
if url:
print(f" Get yours at {url}")
val = _prompt(desc, secret=True)
if val and env_var:
env_writes[env_var] = val
else:
effective_default = provider_config.get(key) 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
return True
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 = [(name, f"— {desc}") for name, desc, _ in providers]
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, cancel_returns=_CANCELLED)
if selected == _CANCELLED:
_print_cancelled_setup()
return
config = load_config()
if not isinstance(config.get("memory"), dict):
config["memory"] = {}
if selected >= len(providers):
config["memory"]["provider"] = ""
save_config(config)
print("\n ✓ Memory provider: built-in only")
print(" Saved to config.yaml\n")
return
name, _, provider = providers[selected]
_clear_interactive_transition()
_install_dependencies(name)
if _post_setup_hook(provider, config):
return
provider_config = config["memory"].get(name, {})
if not isinstance(provider_config, dict):
provider_config = {}
env_writes: dict = {}
schema = _schema_of(provider)
if schema and not _prompt_schema_fields(name, schema, provider_config, env_writes):
return
# Write activation key to config.yaml
config["memory"]["provider"] = name
save_config(config)
if provider_config and hasattr(provider, "save_config"):
try:
provider.save_config(provider_config, str(get_hermes_home()))
except Exception as e:
print(f" Failed to write provider config: {e}")
if env_writes:
_write_env_vars(env_writes)
print(f"\n Memory provider: {name}")
print(" Activation saved to config.yaml")
if provider_config:
print(" Provider config saved")
if env_writes:
print(" API keys saved to .env")
print("\n Start a new session to activate.\n")
def _write_env_vars(
env_writes: dict, hermes_home: str | os.PathLike[str] | None = None) -> None:
"""Persist memory-provider env vars through the canonical ``.env`` writer.
Delegates to ``hermes_cli.config.save_env_value`` so every key flows
through the same input-validation gate as every other ``.env`` writer:
the ``_ENV_VAR_NAME_RE`` regex (no malformed identifiers), the
``_ENV_VAR_NAME_DENYLIST`` (no ``LD_PRELOAD`` / ``PYTHONPATH`` /
``HERMES_HOME`` / etc.), CR/LF stripping on the value, and the atomic
0o600-from-creation write (no TOCTOU permission window).
Validation failures (``ValueError`` from ``save_env_value`` — a
denylisted name or an identifier rejected by ``_ENV_VAR_NAME_RE``) are
surfaced and skipped rather than aborting the wizard, so a single bad
key from one schema field doesn't take down the rest of the batch.
Non-validation errors (filesystem failures, permission errors) are
intentionally NOT caught — those indicate the wizard cannot safely
persist any subsequent key either and should propagate.
``hermes_home`` may be supplied by plugin ``post_setup`` hooks that
already received an explicit home directory (e.g. a non-default
profile). It is applied through the context-local Hermes home override
so ``save_env_value`` still owns the validation, sanitization, and
atomic-write path without mutating global ``os.environ``.
"""
from hermes_cli.config import save_env_value
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
token = set_hermes_home_override(hermes_home) if hermes_home is not None else None
try:
for key, val in env_writes.items():
try:
save_env_value(key, val)
except ValueError as exc:
print(f" Skipping {key}: {exc}")
finally:
if token is not None:
reset_hermes_home_override(token)
def _mark(enabled) -> str:
return "enabled ✓" if enabled else "disabled ✗"
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", "")
# Memory tool enablement for the CLI platform via the canonical resolver, respecting the
# check_fn gate when both stores are disabled.
from hermes_cli.tools_config import _get_platform_tools
from tools.memory_tool import check_memory_requirements
cli_tools = _get_platform_tools(config, "cli", include_default_mcp_servers=False)
memory_tool_enabled = ("memory" in cli_tools) and check_memory_requirements()
print("\nMemory status\n" + "─" * 40)
print(" Built-in (MEMORY.md / USER.md):")
print(f" Memory injection: {_mark(mem_config.get('memory_enabled', True))}")
print(f" User profile: {_mark(mem_config.get('user_profile_enabled', True))}")
print(f" Memory tool: {_mark(memory_tool_enabled)}")
print(f" Provider: {provider_name or '(none — built-in only)'}")
providers = _get_available_providers()
match = _find_provider(providers, provider_name)
provider = match[2] if match else None
if provider_name:
provider_config = mem_config.get(provider_name, {})
display_config = provider_config
if provider and hasattr(provider, "get_status_config"):
try:
display_config = provider.get_status_config(provider_config)
except Exception as e:
display_config = dict(provider_config) if isinstance(provider_config, dict) else provider_config
if isinstance(display_config, dict):
display_config["status_config_error"] = str(e)
if display_config:
print(f"\n {provider_name} config:")
for key, val in display_config.items():
print(f" {key}: {val}")
if provider:
print("\n Plugin: installed ✓")
if provider.is_available():
print(" Status: available ✓")
else:
print(" Status: not available ✗")
# All fields with env_var (secret and non-secret)
required_fields = [f for f in _schema_of(provider) if f.get("env_var")]
if required_fields:
print(" Missing:")
for f in required_fields:
env_var = f.get("env_var", "")
url = f.get("url", "")
is_set = bool(os.environ.get(env_var))
line = f" {'✓' if is_set else '✗'} {env_var}"
if url and not is_set:
line += f" → {url}"
print(line)
print(" Note: systemd/gateway services do not inherit ~/.hermes/.env —")
print(" set any variables above in the service environment.")
else:
print("\n Plugin: NOT installed ✗")
print(f" Install the '{provider_name}' memory plugin to ~/.hermes/plugins/")
if providers:
print("\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."""
if getattr(args, "memory_command", None) == "setup":
provider = getattr(args, "provider", None)
if provider:
cmd_setup_provider(provider)
else:
cmd_setup(args)
else:
cmd_status(args)