# 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
467 lines
18 KiB
Python
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)
|