mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-18 09:51:59 +00:00
feat(memory): improve OpenViking setup UX
Support linking, copying, and creating ovcli.conf during OpenViking memory setup. Make setup cancellation write nothing and cover OpenViking/Hindsight picker cancellation paths.
This commit is contained in:
parent
c6e99ab375
commit
2dace37f6b
8 changed files with 818 additions and 55 deletions
|
|
@ -15,24 +15,40 @@ from pathlib import Path
|
|||
from hermes_constants import get_hermes_home
|
||||
from hermes_cli.secret_prompt import masked_secret_prompt
|
||||
|
||||
_CANCELLED = -1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Curses-based interactive picker (same pattern as hermes tools)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _curses_select(title: str, items: list[tuple[str, str]], default: int = 0) -> int:
|
||||
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.
|
||||
|
||||
items: list of (label, description) tuples.
|
||||
Returns selected index, or default on escape/quit.
|
||||
Returns selected index, or cancel_returns/default on escape/quit.
|
||||
"""
|
||||
from hermes_cli.curses_ui import curses_radiolist
|
||||
|
||||
if cancel_returns is None:
|
||||
cancel_returns = default
|
||||
|
||||
# Format (label, desc) tuples into display strings
|
||||
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)
|
||||
return curses_radiolist(title, display_items, selected=default, cancel_returns=cancel_returns)
|
||||
|
||||
|
||||
def _print_cancelled_setup() -> None:
|
||||
print("\n Cancelled. No changes saved.\n")
|
||||
|
||||
|
||||
def _prompt(label: str, default: str | None = None, secret: bool = False) -> str:
|
||||
|
|
@ -241,14 +257,17 @@ def cmd_setup(args) -> None:
|
|||
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)
|
||||
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"] = {}
|
||||
|
||||
# Built-in only
|
||||
if selected >= len(providers) or selected < 0:
|
||||
if selected >= len(providers):
|
||||
config["memory"]["provider"] = ""
|
||||
save_config(config)
|
||||
print("\n ✓ Memory provider: built-in only")
|
||||
|
|
@ -309,7 +328,10 @@ def cmd_setup(args) -> None:
|
|||
current_idx = 0
|
||||
if current and current in choices:
|
||||
current_idx = choices.index(current)
|
||||
sel = _curses_select(f" {desc}", choice_items, default=current_idx)
|
||||
sel = _curses_select(f" {desc}", choice_items, default=current_idx, cancel_returns=_CANCELLED)
|
||||
if sel == _CANCELLED:
|
||||
_print_cancelled_setup()
|
||||
return
|
||||
provider_config[key] = choices[sel]
|
||||
elif is_secret:
|
||||
# Prompt for secret
|
||||
|
|
|
|||
|
|
@ -702,7 +702,7 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
from hermes_cli.config import save_config
|
||||
from hermes_cli.secret_prompt import masked_secret_prompt
|
||||
|
||||
from hermes_cli.memory_setup import _curses_select
|
||||
from hermes_cli.memory_setup import _CANCELLED, _curses_select, _print_cancelled_setup
|
||||
|
||||
print("\n Configuring Hindsight memory:\n")
|
||||
|
||||
|
|
@ -719,7 +719,10 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
]
|
||||
existing_mode = existing_config.get("mode")
|
||||
mode_default_idx = mode_values.index(existing_mode) if existing_mode in mode_values else 0
|
||||
mode_idx = _curses_select(" Select mode", mode_items, default=mode_default_idx)
|
||||
mode_idx = _curses_select(" Select mode", mode_items, default=mode_default_idx, cancel_returns=_CANCELLED)
|
||||
if mode_idx == _CANCELLED:
|
||||
_print_cancelled_setup()
|
||||
return
|
||||
mode = mode_values[mode_idx]
|
||||
|
||||
provider_config: dict = dict(existing_config)
|
||||
|
|
@ -737,6 +740,27 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
else:
|
||||
deps_to_install = [cloud_dep]
|
||||
|
||||
llm_provider = ""
|
||||
if mode == "local_embedded":
|
||||
providers_list = list(_PROVIDER_DEFAULT_MODELS.keys())
|
||||
llm_items = [
|
||||
(p, f"default model: {_PROVIDER_DEFAULT_MODELS[p]}")
|
||||
for p in providers_list
|
||||
]
|
||||
existing_llm_provider = provider_config.get("llm_provider")
|
||||
llm_default_idx = providers_list.index(existing_llm_provider) if existing_llm_provider in providers_list else 0
|
||||
llm_idx = _curses_select(
|
||||
" Select LLM provider",
|
||||
llm_items,
|
||||
default=llm_default_idx,
|
||||
cancel_returns=_CANCELLED,
|
||||
)
|
||||
if llm_idx == _CANCELLED:
|
||||
_print_cancelled_setup()
|
||||
return
|
||||
llm_provider = providers_list[llm_idx]
|
||||
provider_config["llm_provider"] = llm_provider
|
||||
|
||||
print("\n Checking dependencies...")
|
||||
uv_path = shutil.which("uv")
|
||||
if not uv_path:
|
||||
|
|
@ -785,18 +809,6 @@ class HindsightMemoryProvider(MemoryProvider):
|
|||
env_writes["HINDSIGHT_API_KEY"] = api_key
|
||||
|
||||
else: # local_embedded
|
||||
providers_list = list(_PROVIDER_DEFAULT_MODELS.keys())
|
||||
llm_items = [
|
||||
(p, f"default model: {_PROVIDER_DEFAULT_MODELS[p]}")
|
||||
for p in providers_list
|
||||
]
|
||||
existing_llm_provider = provider_config.get("llm_provider")
|
||||
llm_default_idx = providers_list.index(existing_llm_provider) if existing_llm_provider in providers_list else 0
|
||||
llm_idx = _curses_select(" Select LLM provider", llm_items, default=llm_default_idx)
|
||||
llm_provider = providers_list[llm_idx]
|
||||
|
||||
provider_config["llm_provider"] = llm_provider
|
||||
|
||||
if llm_provider == "openai_compatible":
|
||||
existing_base_url = provider_config.get("llm_base_url", "")
|
||||
prompt = " LLM endpoint URL (e.g. http://192.168.1.10:8080/v1)"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ Context database by Volcengine (ByteDance) with filesystem-style knowledge hiera
|
|||
hermes memory setup # select "openviking"
|
||||
```
|
||||
|
||||
The setup can link to an existing `~/.openviking/ovcli.conf`, copy its current
|
||||
connection values into Hermes, or create a minimal `ovcli.conf` when one does
|
||||
not exist.
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
hermes config set memory.provider openviking
|
||||
|
|
@ -28,6 +32,9 @@ All config via environment variables in `.env`:
|
|||
|---------|---------|-------------|
|
||||
| `OPENVIKING_ENDPOINT` | `http://127.0.0.1:1933` | Server URL |
|
||||
| `OPENVIKING_API_KEY` | (none) | API key (optional) |
|
||||
| `OPENVIKING_ACCOUNT` | (none) | Tenant account override |
|
||||
| `OPENVIKING_USER` | (none) | Tenant user override |
|
||||
| `OPENVIKING_AGENT` | `hermes` | Tenant agent namespace |
|
||||
|
||||
## Tools
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ automatic memory extraction, and session management.
|
|||
Original PR #3369 by Mibayy, rewritten to use the full OpenViking session
|
||||
lifecycle instead of read-only search endpoints.
|
||||
|
||||
Config via environment variables (profile-scoped via each profile's .env):
|
||||
Config via environment variables (profile-scoped via each profile's .env)
|
||||
or a linked OpenViking CLI config:
|
||||
OPENVIKING_ENDPOINT — Server URL (default: http://127.0.0.1:1933)
|
||||
OPENVIKING_API_KEY — API key (required for authenticated servers)
|
||||
OPENVIKING_ACCOUNT — Tenant account (default: default)
|
||||
OPENVIKING_USER — Tenant user (default: default)
|
||||
OPENVIKING_AGENT — Tenant agent (default: hermes)
|
||||
OPENVIKING_ACCOUNT — Optional tenant account override
|
||||
OPENVIKING_USER — Optional tenant user override
|
||||
OPENVIKING_AGENT — Tenant agent (default: hermes)
|
||||
|
||||
Capabilities:
|
||||
- Automatic memory extraction on session commit (6 categories)
|
||||
|
|
@ -44,6 +45,18 @@ from tools.registry import tool_error
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
|
||||
_DEFAULT_ACCOUNT = ""
|
||||
_DEFAULT_USER = ""
|
||||
_DEFAULT_AGENT = "hermes"
|
||||
_OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
|
||||
_OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
|
||||
_OPENVIKING_ENV_KEYS = (
|
||||
"OPENVIKING_ENDPOINT",
|
||||
"OPENVIKING_API_KEY",
|
||||
"OPENVIKING_ACCOUNT",
|
||||
"OPENVIKING_USER",
|
||||
"OPENVIKING_AGENT",
|
||||
)
|
||||
_TIMEOUT = 30.0
|
||||
_REMOTE_RESOURCE_PREFIXES = ("http://", "https://", "git@", "ssh://", "git://")
|
||||
|
||||
|
|
@ -108,27 +121,21 @@ class _VikingClient:
|
|||
"""Thin HTTP client for the OpenViking REST API."""
|
||||
|
||||
def __init__(self, endpoint: str, api_key: str = "",
|
||||
account: str = "", user: str = "", agent: str = ""):
|
||||
account: Optional[str] = None, user: Optional[str] = None,
|
||||
agent: Optional[str] = None):
|
||||
self._endpoint = endpoint.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
|
||||
self._user = user or os.environ.get("OPENVIKING_USER", "default")
|
||||
self._agent = agent or os.environ.get("OPENVIKING_AGENT", "hermes")
|
||||
self._account = account if account is not None else os.environ.get("OPENVIKING_ACCOUNT", _DEFAULT_ACCOUNT)
|
||||
self._user = user if user is not None else os.environ.get("OPENVIKING_USER", _DEFAULT_USER)
|
||||
self._agent = agent if agent is not None else os.environ.get("OPENVIKING_AGENT", _DEFAULT_AGENT)
|
||||
self._httpx = _get_httpx()
|
||||
if self._httpx is None:
|
||||
raise ImportError("httpx is required for OpenViking: pip install httpx")
|
||||
|
||||
def _headers(self) -> dict:
|
||||
# Always send tenant headers when account/user are configured.
|
||||
# OpenViking 0.3.x requires X-OpenViking-Account and X-OpenViking-User
|
||||
# for ROOT API key requests to tenant-scoped APIs — omitting them
|
||||
# causes INVALID_ARGUMENT errors even when account="default".
|
||||
# User-level keys can omit them (server derives tenancy from the key),
|
||||
# but ROOT keys must always include them explicitly.
|
||||
h = {
|
||||
"Content-Type": "application/json",
|
||||
"X-OpenViking-Agent": self._agent,
|
||||
}
|
||||
h = {"Content-Type": "application/json"}
|
||||
if self._agent:
|
||||
h["X-OpenViking-Agent"] = self._agent
|
||||
if self._account:
|
||||
h["X-OpenViking-Account"] = self._account
|
||||
if self._user:
|
||||
|
|
@ -405,6 +412,156 @@ def _path_from_file_uri(uri: str) -> Path | str:
|
|||
return Path(url2pathname(parsed.path)).expanduser()
|
||||
|
||||
|
||||
def _clean_config_value(value: Any) -> str:
|
||||
return value.strip() if isinstance(value, str) else ""
|
||||
|
||||
|
||||
def _default_ovcli_config_path() -> Path:
|
||||
return Path.home() / _OVCLI_DEFAULT_RELATIVE_PATH
|
||||
|
||||
|
||||
def _resolve_ovcli_config_path(config_path: str = "") -> Path:
|
||||
if config_path:
|
||||
return Path(config_path).expanduser()
|
||||
env_path = os.environ.get(_OVCLI_CONFIG_ENV, "").strip()
|
||||
if env_path:
|
||||
return Path(env_path).expanduser()
|
||||
return _default_ovcli_config_path()
|
||||
|
||||
|
||||
def _load_ovcli_config(path: Optional[Path] = None) -> dict:
|
||||
config_path = path or _resolve_ovcli_config_path()
|
||||
if not config_path.exists():
|
||||
return {}
|
||||
with config_path.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"OpenViking CLI config must be a JSON object: {config_path}")
|
||||
return data
|
||||
|
||||
|
||||
def _connection_values_from_ovcli(data: dict) -> dict:
|
||||
return {
|
||||
"endpoint": _clean_config_value(data.get("url")) or _DEFAULT_ENDPOINT,
|
||||
"api_key": _clean_config_value(data.get("api_key")),
|
||||
"account": _clean_config_value(data.get("account") or data.get("account_id")),
|
||||
"user": _clean_config_value(data.get("user") or data.get("user_id")),
|
||||
"agent": _clean_config_value(data.get("agent_id")),
|
||||
}
|
||||
|
||||
|
||||
def _load_hermes_openviking_config() -> dict:
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
|
||||
config = load_config()
|
||||
memory_config = config.get("memory", {}) if isinstance(config, dict) else {}
|
||||
provider_config = memory_config.get("openviking", {}) if isinstance(memory_config, dict) else {}
|
||||
return dict(provider_config) if isinstance(provider_config, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _env_value(name: str) -> Optional[str]:
|
||||
return os.environ[name].strip() if name in os.environ else None
|
||||
|
||||
|
||||
def _first_nonempty(*values: Optional[str], default: str = "") -> str:
|
||||
for value in values:
|
||||
if value:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def _resolve_connection_settings(provider_config: Optional[dict] = None) -> dict:
|
||||
provider_config = dict(provider_config or {})
|
||||
ovcli_values: dict = {}
|
||||
if provider_config.get("use_ovcli_config"):
|
||||
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
||||
ovcli_values = _connection_values_from_ovcli(_load_ovcli_config(ovcli_path))
|
||||
|
||||
endpoint_env = _env_value("OPENVIKING_ENDPOINT")
|
||||
api_key_env = _env_value("OPENVIKING_API_KEY")
|
||||
account_env = _env_value("OPENVIKING_ACCOUNT")
|
||||
user_env = _env_value("OPENVIKING_USER")
|
||||
agent_env = _env_value("OPENVIKING_AGENT")
|
||||
|
||||
return {
|
||||
"endpoint": _first_nonempty(endpoint_env, ovcli_values.get("endpoint"), default=_DEFAULT_ENDPOINT),
|
||||
"api_key": api_key_env if api_key_env is not None else ovcli_values.get("api_key", ""),
|
||||
"account": account_env if account_env is not None else ovcli_values.get("account", ""),
|
||||
"user": user_env if user_env is not None else ovcli_values.get("user", ""),
|
||||
"agent": _first_nonempty(agent_env, ovcli_values.get("agent"), default=_DEFAULT_AGENT),
|
||||
}
|
||||
|
||||
|
||||
def _env_writes_from_connection_values(values: dict) -> dict:
|
||||
writes = {}
|
||||
mapping = {
|
||||
"OPENVIKING_ENDPOINT": "endpoint",
|
||||
"OPENVIKING_API_KEY": "api_key",
|
||||
"OPENVIKING_ACCOUNT": "account",
|
||||
"OPENVIKING_USER": "user",
|
||||
"OPENVIKING_AGENT": "agent",
|
||||
}
|
||||
for env_key, value_key in mapping.items():
|
||||
value = _clean_config_value(values.get(value_key))
|
||||
if value:
|
||||
writes[env_key] = value
|
||||
return writes
|
||||
|
||||
|
||||
def _write_env_vars(env_path: Path, env_writes: dict, remove_keys: tuple[str, ...] = ()) -> None:
|
||||
env_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
remove_set = set(remove_keys) - set(env_writes)
|
||||
existing_lines = env_path.read_text(encoding="utf-8").splitlines() if env_path.exists() else []
|
||||
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 remove_set:
|
||||
continue
|
||||
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" if new_lines else ""), encoding="utf-8")
|
||||
|
||||
|
||||
def _remember_ovcli_path(provider_config: dict, ovcli_path: Path) -> None:
|
||||
default_path = _default_ovcli_config_path().expanduser()
|
||||
if os.environ.get(_OVCLI_CONFIG_ENV, "").strip() or ovcli_path.expanduser() != default_path:
|
||||
provider_config["ovcli_config_path"] = str(ovcli_path)
|
||||
else:
|
||||
provider_config.pop("ovcli_config_path", None)
|
||||
|
||||
|
||||
def _ovcli_data_from_connection_values(values: dict) -> dict:
|
||||
data = {"url": _clean_config_value(values.get("endpoint")) or _DEFAULT_ENDPOINT}
|
||||
api_key = _clean_config_value(values.get("api_key"))
|
||||
account = _clean_config_value(values.get("account"))
|
||||
user = _clean_config_value(values.get("user"))
|
||||
agent = _clean_config_value(values.get("agent")) or _DEFAULT_AGENT
|
||||
if api_key:
|
||||
data["api_key"] = api_key
|
||||
if account:
|
||||
data["account"] = account
|
||||
if user:
|
||||
data["user"] = user
|
||||
if agent:
|
||||
data["agent_id"] = agent
|
||||
return data
|
||||
|
||||
|
||||
def _write_ovcli_config(path: Path, values: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(_ovcli_data_from_connection_values(values), indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MemoryProvider implementation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -429,7 +586,16 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if OpenViking endpoint is configured. No network calls."""
|
||||
return bool(os.environ.get("OPENVIKING_ENDPOINT"))
|
||||
if os.environ.get("OPENVIKING_ENDPOINT"):
|
||||
return True
|
||||
provider_config = _load_hermes_openviking_config()
|
||||
if not provider_config.get("use_ovcli_config"):
|
||||
return False
|
||||
try:
|
||||
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
||||
return bool(_connection_values_from_ovcli(_load_ovcli_config(ovcli_path)).get("endpoint"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_config_schema(self):
|
||||
return [
|
||||
|
|
@ -448,14 +614,12 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
},
|
||||
{
|
||||
"key": "account",
|
||||
"description": "OpenViking tenant account ID ([default], used when local mode, OPENVIKING_API_KEY is empty)",
|
||||
"default": "default",
|
||||
"description": "OpenViking tenant account ID (blank for user API keys)",
|
||||
"env_var": "OPENVIKING_ACCOUNT",
|
||||
},
|
||||
{
|
||||
"key": "user",
|
||||
"description": "OpenViking user ID within the account ([default], used when local mode, OPENVIKING_API_KEY is empty)",
|
||||
"default": "default",
|
||||
"description": "OpenViking user ID within the account (blank for user API keys)",
|
||||
"env_var": "OPENVIKING_USER",
|
||||
},
|
||||
{
|
||||
|
|
@ -466,12 +630,132 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
|||
},
|
||||
]
|
||||
|
||||
def post_setup(self, hermes_home: str, config: dict) -> None:
|
||||
"""Custom setup that can reuse OpenViking's shared CLI config."""
|
||||
from hermes_cli.config import save_config
|
||||
from hermes_cli.memory_setup import _CANCELLED, _curses_select, _print_cancelled_setup, _prompt
|
||||
|
||||
hermes_home_path = Path(hermes_home)
|
||||
env_path = hermes_home_path / ".env"
|
||||
if not isinstance(config.get("memory"), dict):
|
||||
config["memory"] = {}
|
||||
provider_config = config["memory"].get("openviking", {})
|
||||
if not isinstance(provider_config, dict):
|
||||
provider_config = {}
|
||||
|
||||
ovcli_path = _resolve_ovcli_config_path(str(provider_config.get("ovcli_config_path") or ""))
|
||||
|
||||
print("\n Configuring OpenViking memory:\n")
|
||||
|
||||
if ovcli_path.exists():
|
||||
try:
|
||||
ovcli_values = _connection_values_from_ovcli(_load_ovcli_config(ovcli_path))
|
||||
except Exception as e:
|
||||
print(f"\n Could not read OpenViking CLI config: {e}")
|
||||
print(" No changes saved.\n")
|
||||
return
|
||||
|
||||
setup_options = [
|
||||
("Link to ovcli.conf", "Hermes follows the active OpenViking CLI config"),
|
||||
("Copy once", "Hermes won't follow future ovcli.conf changes"),
|
||||
]
|
||||
choice = _curses_select(
|
||||
" OpenViking config source",
|
||||
setup_options,
|
||||
default=0,
|
||||
cancel_returns=_CANCELLED,
|
||||
)
|
||||
if choice == _CANCELLED:
|
||||
_print_cancelled_setup()
|
||||
return
|
||||
|
||||
if choice == 0:
|
||||
provider_config["use_ovcli_config"] = True
|
||||
_remember_ovcli_path(provider_config, ovcli_path)
|
||||
_write_env_vars(env_path, {}, remove_keys=_OPENVIKING_ENV_KEYS)
|
||||
config["memory"]["provider"] = "openviking"
|
||||
config["memory"]["openviking"] = provider_config
|
||||
save_config(config)
|
||||
print(f"\n Memory provider: openviking")
|
||||
print(f" Linked config: {ovcli_path}")
|
||||
print(" Start a new session to activate.\n")
|
||||
return
|
||||
|
||||
provider_config["use_ovcli_config"] = False
|
||||
provider_config.pop("ovcli_config_path", None)
|
||||
config["memory"]["provider"] = "openviking"
|
||||
config["memory"]["openviking"] = provider_config
|
||||
save_config(config)
|
||||
_write_env_vars(
|
||||
env_path,
|
||||
_env_writes_from_connection_values(ovcli_values),
|
||||
remove_keys=_OPENVIKING_ENV_KEYS,
|
||||
)
|
||||
print(f"\n Memory provider: openviking")
|
||||
print(" Connection saved to .env")
|
||||
print(" Start a new session to activate.\n")
|
||||
return
|
||||
|
||||
setup_options = [
|
||||
("Create ovcli.conf and link", "Recommended"),
|
||||
("Configure Hermes only", "Write OpenViking values to Hermes .env"),
|
||||
]
|
||||
choice = _curses_select(
|
||||
" OpenViking config source",
|
||||
setup_options,
|
||||
default=0,
|
||||
cancel_returns=_CANCELLED,
|
||||
)
|
||||
if choice == _CANCELLED:
|
||||
_print_cancelled_setup()
|
||||
return
|
||||
|
||||
defaults = {
|
||||
"endpoint": _DEFAULT_ENDPOINT,
|
||||
"api_key": "",
|
||||
"account": "",
|
||||
"user": "",
|
||||
"agent": _DEFAULT_AGENT,
|
||||
}
|
||||
values = {
|
||||
"endpoint": _prompt("OpenViking server URL", default=defaults["endpoint"]),
|
||||
"api_key": _prompt("OpenViking API key", secret=True),
|
||||
"account": _prompt("OpenViking account", default=defaults["account"]),
|
||||
"user": _prompt("OpenViking user", default=defaults["user"]),
|
||||
"agent": _prompt("OpenViking agent", default=defaults["agent"]),
|
||||
}
|
||||
|
||||
config["memory"]["provider"] = "openviking"
|
||||
if choice == 0:
|
||||
_write_ovcli_config(ovcli_path, values)
|
||||
provider_config["use_ovcli_config"] = True
|
||||
_remember_ovcli_path(provider_config, ovcli_path)
|
||||
config["memory"]["openviking"] = provider_config
|
||||
save_config(config)
|
||||
_write_env_vars(env_path, {}, remove_keys=_OPENVIKING_ENV_KEYS)
|
||||
print(f"\n Memory provider: openviking")
|
||||
print(f" Created config: {ovcli_path}")
|
||||
else:
|
||||
provider_config["use_ovcli_config"] = False
|
||||
provider_config.pop("ovcli_config_path", None)
|
||||
config["memory"]["openviking"] = provider_config
|
||||
save_config(config)
|
||||
_write_env_vars(
|
||||
env_path,
|
||||
_env_writes_from_connection_values(values),
|
||||
remove_keys=_OPENVIKING_ENV_KEYS,
|
||||
)
|
||||
print(f"\n Memory provider: openviking")
|
||||
print(" Connection saved to .env")
|
||||
print(" Start a new session to activate.\n")
|
||||
|
||||
def initialize(self, session_id: str, **kwargs) -> None:
|
||||
self._endpoint = os.environ.get("OPENVIKING_ENDPOINT", _DEFAULT_ENDPOINT)
|
||||
self._api_key = os.environ.get("OPENVIKING_API_KEY", "")
|
||||
self._account = os.environ.get("OPENVIKING_ACCOUNT", "default")
|
||||
self._user = os.environ.get("OPENVIKING_USER", "default")
|
||||
self._agent = os.environ.get("OPENVIKING_AGENT", "hermes")
|
||||
settings = _resolve_connection_settings(_load_hermes_openviking_config())
|
||||
self._endpoint = settings["endpoint"]
|
||||
self._api_key = settings["api_key"]
|
||||
self._account = settings["account"]
|
||||
self._user = settings["user"]
|
||||
self._agent = settings["agent"]
|
||||
self._session_id = session_id
|
||||
self._turn_count = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ version: 2.0.0
|
|||
description: "OpenViking context database — session-managed memory with automatic extraction, tiered retrieval, and filesystem-style knowledge browsing."
|
||||
pip_dependencies:
|
||||
- httpx
|
||||
requires_env:
|
||||
- OPENVIKING_ENDPOINT
|
||||
requires_env: []
|
||||
hooks:
|
||||
- on_session_end
|
||||
|
|
|
|||
109
tests/hermes_cli/test_memory_setup.py
Normal file
109
tests/hermes_cli/test_memory_setup.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import hermes_cli.memory_setup as memory_setup
|
||||
from hermes_cli.memory_setup import _CANCELLED, _curses_select
|
||||
|
||||
|
||||
def test_curses_select_cancel_defaults_to_selected(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_radiolist(title, items, selected=0, *, cancel_returns=None):
|
||||
captured.update({
|
||||
"title": title,
|
||||
"items": items,
|
||||
"selected": selected,
|
||||
"cancel_returns": cancel_returns,
|
||||
})
|
||||
return cancel_returns
|
||||
|
||||
monkeypatch.setattr("hermes_cli.curses_ui.curses_radiolist", fake_radiolist)
|
||||
|
||||
result = _curses_select("Pick one", [("first", "desc"), ("second", "")], default=1)
|
||||
|
||||
assert result == 1
|
||||
assert captured == {
|
||||
"title": "Pick one",
|
||||
"items": ["first desc", "second"],
|
||||
"selected": 1,
|
||||
"cancel_returns": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_curses_select_accepts_explicit_cancel_value(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
def fake_radiolist(title, items, selected=0, *, cancel_returns=None):
|
||||
captured["cancel_returns"] = cancel_returns
|
||||
return cancel_returns
|
||||
|
||||
monkeypatch.setattr("hermes_cli.curses_ui.curses_radiolist", fake_radiolist)
|
||||
|
||||
result = _curses_select("Pick one", [("first", "")], default=0, cancel_returns=_CANCELLED)
|
||||
|
||||
assert result == _CANCELLED
|
||||
assert captured["cancel_returns"] == _CANCELLED
|
||||
|
||||
|
||||
def test_cmd_setup_top_level_cancel_writes_nothing(monkeypatch):
|
||||
save_config = MagicMock()
|
||||
load_config = MagicMock(side_effect=AssertionError("cancel should not load config"))
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_get_available_providers", lambda: [("fake", "local", object())])
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: kwargs["cancel_returns"])
|
||||
monkeypatch.setattr("hermes_cli.config.load_config", load_config)
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", save_config)
|
||||
|
||||
memory_setup.cmd_setup(SimpleNamespace())
|
||||
|
||||
load_config.assert_not_called()
|
||||
save_config.assert_not_called()
|
||||
|
||||
|
||||
def test_cmd_setup_builtin_selection_still_saves_builtin(monkeypatch):
|
||||
save_config = MagicMock()
|
||||
config = {"memory": {"provider": "openviking"}}
|
||||
providers = [("fake", "local", object())]
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_get_available_providers", lambda: providers)
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: len(providers))
|
||||
monkeypatch.setattr("hermes_cli.config.load_config", lambda: config)
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", save_config)
|
||||
|
||||
memory_setup.cmd_setup(SimpleNamespace())
|
||||
|
||||
assert config["memory"]["provider"] == ""
|
||||
save_config.assert_called_once_with(config)
|
||||
|
||||
|
||||
def test_cmd_setup_generic_choice_cancel_writes_nothing(tmp_path, monkeypatch):
|
||||
class ChoiceProvider:
|
||||
def __init__(self):
|
||||
self.save_config = MagicMock()
|
||||
|
||||
def get_config_schema(self):
|
||||
return [{
|
||||
"key": "mode",
|
||||
"description": "Mode",
|
||||
"default": "one",
|
||||
"choices": ["one", "two"],
|
||||
}]
|
||||
|
||||
provider = ChoiceProvider()
|
||||
selections = iter([0, _CANCELLED])
|
||||
save_config = MagicMock()
|
||||
install_dependencies = MagicMock()
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_get_available_providers", lambda: [("fake", "local", provider)])
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: next(selections))
|
||||
monkeypatch.setattr(memory_setup, "_install_dependencies", install_dependencies)
|
||||
monkeypatch.setattr(memory_setup, "get_hermes_home", lambda: tmp_path)
|
||||
monkeypatch.setattr("hermes_cli.config.load_config", lambda: {"memory": {}})
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", save_config)
|
||||
|
||||
memory_setup.cmd_setup(SimpleNamespace())
|
||||
|
||||
install_dependencies.assert_called_once_with("fake")
|
||||
save_config.assert_not_called()
|
||||
provider.save_config.assert_not_called()
|
||||
assert not (tmp_path / ".env").exists()
|
||||
|
|
@ -15,6 +15,7 @@ from unittest.mock import AsyncMock, MagicMock
|
|||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.memory_setup import _CANCELLED
|
||||
from plugins.memory.hindsight import (
|
||||
HindsightMemoryProvider,
|
||||
RECALL_SCHEMA,
|
||||
|
|
@ -376,6 +377,61 @@ class TestConfig:
|
|||
|
||||
|
||||
class TestPostSetup:
|
||||
def test_setup_cancel_at_mode_picker_writes_nothing(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes-home"
|
||||
user_home = tmp_path / "user-home"
|
||||
user_home.mkdir()
|
||||
monkeypatch.setenv("HOME", str(user_home))
|
||||
monkeypatch.setattr("plugins.memory.hindsight.get_hermes_home", lambda: hermes_home)
|
||||
|
||||
save_config = MagicMock()
|
||||
which = MagicMock(return_value="/usr/bin/uv")
|
||||
run = MagicMock()
|
||||
monkeypatch.setattr("hermes_cli.memory_setup._curses_select", lambda *args, **kwargs: _CANCELLED)
|
||||
monkeypatch.setattr("shutil.which", which)
|
||||
monkeypatch.setattr("subprocess.run", run)
|
||||
monkeypatch.setattr("builtins.input", MagicMock(side_effect=AssertionError("prompt should not run")))
|
||||
monkeypatch.setattr("getpass.getpass", MagicMock(side_effect=AssertionError("prompt should not run")))
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", save_config)
|
||||
|
||||
provider = HindsightMemoryProvider()
|
||||
provider.post_setup(str(hermes_home), {"memory": {"provider": "builtin"}})
|
||||
|
||||
save_config.assert_not_called()
|
||||
which.assert_not_called()
|
||||
run.assert_not_called()
|
||||
assert not (hermes_home / ".env").exists()
|
||||
assert not (hermes_home / "hindsight" / "config.json").exists()
|
||||
assert not (user_home / ".hindsight" / "profiles" / "hermes.env").exists()
|
||||
|
||||
def test_local_embedded_setup_cancel_at_llm_picker_writes_nothing(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes-home"
|
||||
user_home = tmp_path / "user-home"
|
||||
user_home.mkdir()
|
||||
monkeypatch.setenv("HOME", str(user_home))
|
||||
monkeypatch.setattr("plugins.memory.hindsight.get_hermes_home", lambda: hermes_home)
|
||||
|
||||
selections = iter([1, _CANCELLED]) # local_embedded, then cancel LLM picker
|
||||
save_config = MagicMock()
|
||||
which = MagicMock(return_value="/usr/bin/uv")
|
||||
run = MagicMock()
|
||||
monkeypatch.setattr("hermes_cli.memory_setup._curses_select", lambda *args, **kwargs: next(selections))
|
||||
monkeypatch.setattr("shutil.which", which)
|
||||
monkeypatch.setattr("subprocess.run", run)
|
||||
monkeypatch.setattr("builtins.input", MagicMock(side_effect=AssertionError("prompt should not run")))
|
||||
monkeypatch.setattr("getpass.getpass", MagicMock(side_effect=AssertionError("prompt should not run")))
|
||||
monkeypatch.setattr("hermes_cli.config.save_config", save_config)
|
||||
|
||||
provider = HindsightMemoryProvider()
|
||||
provider.post_setup(str(hermes_home), {"memory": {"provider": "builtin"}})
|
||||
|
||||
save_config.assert_not_called()
|
||||
which.assert_not_called()
|
||||
run.assert_not_called()
|
||||
assert not (hermes_home / ".env").exists()
|
||||
assert not (hermes_home / "hindsight" / "config.json").exists()
|
||||
assert not (user_home / ".hindsight" / "profiles" / "hermes.env").exists()
|
||||
|
||||
def test_local_embedded_setup_materializes_profile_env(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes-home"
|
||||
user_home = tmp_path / "user-home"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,281 @@ import pytest
|
|||
from plugins.memory.openviking import OpenVikingMemoryProvider, _VikingClient
|
||||
|
||||
|
||||
def _clear_openviking_env(monkeypatch):
|
||||
for key in (
|
||||
"OPENVIKING_ENDPOINT",
|
||||
"OPENVIKING_API_KEY",
|
||||
"OPENVIKING_ACCOUNT",
|
||||
"OPENVIKING_USER",
|
||||
"OPENVIKING_AGENT",
|
||||
"OPENVIKING_CLI_CONFIG_FILE",
|
||||
):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
def test_linked_ovcli_config_is_read_at_runtime(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text(
|
||||
json.dumps({
|
||||
"url": "http://openviking-one.local",
|
||||
"api_key": "key-one",
|
||||
"account": "acct-one",
|
||||
"user": "alice",
|
||||
"agent_id": "agent-one",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
provider_config = {"use_ovcli_config": True, "ovcli_config_path": str(ovcli_path)}
|
||||
|
||||
settings = openviking_module._resolve_connection_settings(provider_config)
|
||||
|
||||
assert settings == {
|
||||
"endpoint": "http://openviking-one.local",
|
||||
"api_key": "key-one",
|
||||
"account": "acct-one",
|
||||
"user": "alice",
|
||||
"agent": "agent-one",
|
||||
}
|
||||
|
||||
ovcli_path.write_text(
|
||||
json.dumps({
|
||||
"url": "http://openviking-two.local",
|
||||
"api_key": "key-two",
|
||||
"agent_id": "agent-two",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
settings = openviking_module._resolve_connection_settings(provider_config)
|
||||
|
||||
assert settings == {
|
||||
"endpoint": "http://openviking-two.local",
|
||||
"api_key": "key-two",
|
||||
"account": "",
|
||||
"user": "",
|
||||
"agent": "agent-two",
|
||||
}
|
||||
|
||||
|
||||
def test_openviking_env_overrides_linked_ovcli_config(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text(
|
||||
json.dumps({
|
||||
"url": "http://openviking.local",
|
||||
"api_key": "file-key",
|
||||
"account": "file-account",
|
||||
"user": "file-user",
|
||||
"agent_id": "file-agent",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("OPENVIKING_ENDPOINT", "http://env.local")
|
||||
monkeypatch.setenv("OPENVIKING_API_KEY", "env-key")
|
||||
monkeypatch.setenv("OPENVIKING_ACCOUNT", "env-account")
|
||||
monkeypatch.setenv("OPENVIKING_USER", "env-user")
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", "env-agent")
|
||||
|
||||
settings = openviking_module._resolve_connection_settings({
|
||||
"use_ovcli_config": True,
|
||||
"ovcli_config_path": str(ovcli_path),
|
||||
})
|
||||
|
||||
assert settings == {
|
||||
"endpoint": "http://env.local",
|
||||
"api_key": "env-key",
|
||||
"account": "env-account",
|
||||
"user": "env-user",
|
||||
"agent": "env-agent",
|
||||
}
|
||||
|
||||
|
||||
def test_post_setup_link_existing_ovcli_clears_hermes_env(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
env_path = hermes_home / ".env"
|
||||
env_path.write_text(
|
||||
"OPENVIKING_ENDPOINT=http://old.local\n"
|
||||
"OPENVIKING_ACCOUNT=old-account\n"
|
||||
"OTHER_KEY=keep\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text(json.dumps({"url": "http://openviking.local"}), encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: 0)
|
||||
config = {"memory": {}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
assert config["memory"]["provider"] == "openviking"
|
||||
assert config["memory"]["openviking"]["use_ovcli_config"] is True
|
||||
assert config["memory"]["openviking"]["ovcli_config_path"] == str(ovcli_path)
|
||||
env_text = env_path.read_text(encoding="utf-8")
|
||||
assert "OPENVIKING_" not in env_text
|
||||
assert "OTHER_KEY=keep" in env_text
|
||||
|
||||
|
||||
def test_post_setup_copy_existing_ovcli_writes_hermes_env(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text(
|
||||
json.dumps({
|
||||
"url": "http://openviking.local",
|
||||
"api_key": "test-key",
|
||||
"account": "acct",
|
||||
"user": "alice",
|
||||
"agent_id": "agent",
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: 1)
|
||||
config = {"memory": {}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
assert config["memory"]["provider"] == "openviking"
|
||||
assert config["memory"]["openviking"]["use_ovcli_config"] is False
|
||||
env_text = (hermes_home / ".env").read_text(encoding="utf-8")
|
||||
assert "OPENVIKING_ENDPOINT=http://openviking.local" in env_text
|
||||
assert "OPENVIKING_API_KEY=test-key" in env_text
|
||||
assert "OPENVIKING_ACCOUNT=acct" in env_text
|
||||
assert "OPENVIKING_USER=alice" in env_text
|
||||
assert "OPENVIKING_AGENT=agent" in env_text
|
||||
|
||||
|
||||
def test_post_setup_cancel_existing_ovcli_writes_nothing(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
env_path = hermes_home / ".env"
|
||||
original_env = "OPENVIKING_ENDPOINT=http://old.local\nOTHER_KEY=keep\n"
|
||||
env_path.write_text(original_env, encoding="utf-8")
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text(json.dumps({"url": "http://openviking.local"}), encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import config as hermes_config
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
save_config = MagicMock()
|
||||
monkeypatch.setattr(hermes_config, "save_config", save_config)
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: -1)
|
||||
config = {"memory": {"provider": "builtin"}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
save_config.assert_not_called()
|
||||
assert config == {"memory": {"provider": "builtin"}}
|
||||
assert env_path.read_text(encoding="utf-8") == original_env
|
||||
|
||||
|
||||
def test_post_setup_invalid_existing_ovcli_writes_nothing(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
env_path = hermes_home / ".env"
|
||||
original_env = "OPENVIKING_ENDPOINT=http://old.local\nOTHER_KEY=keep\n"
|
||||
env_path.write_text(original_env, encoding="utf-8")
|
||||
ovcli_path = tmp_path / "ovcli.conf"
|
||||
ovcli_path.write_text("{", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import config as hermes_config
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
save_config = MagicMock()
|
||||
monkeypatch.setattr(hermes_config, "save_config", save_config)
|
||||
monkeypatch.setattr(
|
||||
memory_setup,
|
||||
"_curses_select",
|
||||
MagicMock(side_effect=AssertionError("picker should not open for invalid ovcli.conf")),
|
||||
)
|
||||
config = {"memory": {"provider": "builtin"}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
save_config.assert_not_called()
|
||||
assert config == {"memory": {"provider": "builtin"}}
|
||||
assert env_path.read_text(encoding="utf-8") == original_env
|
||||
|
||||
|
||||
def test_post_setup_creates_minimal_ovcli_and_links(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
ovcli_path = tmp_path / "missing" / "ovcli.conf"
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: 0)
|
||||
monkeypatch.setattr(
|
||||
memory_setup,
|
||||
"_prompt",
|
||||
lambda label, default=None, secret=False: default or "",
|
||||
)
|
||||
config = {"memory": {}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
assert config["memory"]["provider"] == "openviking"
|
||||
assert config["memory"]["openviking"]["use_ovcli_config"] is True
|
||||
data = json.loads(ovcli_path.read_text(encoding="utf-8"))
|
||||
assert data == {
|
||||
"url": "http://127.0.0.1:1933",
|
||||
"agent_id": "hermes",
|
||||
}
|
||||
env_path = hermes_home / ".env"
|
||||
if env_path.exists():
|
||||
assert env_path.read_text(encoding="utf-8") == ""
|
||||
|
||||
|
||||
def test_post_setup_cancel_missing_ovcli_does_not_prompt_or_create(tmp_path, monkeypatch):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
ovcli_path = tmp_path / "missing" / "ovcli.conf"
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setenv("OPENVIKING_CLI_CONFIG_FILE", str(ovcli_path))
|
||||
|
||||
from hermes_cli import config as hermes_config
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
save_config = MagicMock()
|
||||
monkeypatch.setattr(hermes_config, "save_config", save_config)
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", lambda *args, **kwargs: -1)
|
||||
monkeypatch.setattr(
|
||||
memory_setup,
|
||||
"_prompt",
|
||||
MagicMock(side_effect=AssertionError("prompts should not run after cancel")),
|
||||
)
|
||||
config = {"memory": {"provider": "builtin"}}
|
||||
|
||||
OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
|
||||
|
||||
save_config.assert_not_called()
|
||||
assert config == {"memory": {"provider": "builtin"}}
|
||||
assert not ovcli_path.exists()
|
||||
assert not (hermes_home / ".env").exists()
|
||||
|
||||
|
||||
def test_tool_search_sorts_by_raw_score_across_buckets():
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._client = MagicMock()
|
||||
|
|
@ -371,9 +646,7 @@ def test_viking_client_headers_send_tenant_when_default():
|
|||
assert headers["Authorization"] == "Bearer test-key"
|
||||
|
||||
|
||||
def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default():
|
||||
# Empty account/user strings fall back to "default" via the constructor.
|
||||
# Headers are sent even for the default value — ROOT API keys need them.
|
||||
def test_viking_client_headers_omit_tenant_when_empty():
|
||||
client = _VikingClient(
|
||||
"https://example.com",
|
||||
api_key="",
|
||||
|
|
@ -382,8 +655,9 @@ def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default():
|
|||
agent="hermes",
|
||||
)
|
||||
headers = client._headers()
|
||||
assert headers["X-OpenViking-Account"] == "default"
|
||||
assert headers["X-OpenViking-User"] == "default"
|
||||
assert "X-OpenViking-Account" not in headers
|
||||
assert "X-OpenViking-User" not in headers
|
||||
assert headers["X-OpenViking-Agent"] == "hermes"
|
||||
assert "Authorization" not in headers
|
||||
assert "X-API-Key" not in headers
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue