* feat(hermes): file-based config + updated docs Replace the old dataclass/configure() singleton with a plain dict config loaded from ~/.hindsight/hermes.json — same field names and conventions as the openclaw and claude-code integrations. Loading order: defaults → config file → env var overrides. - config.py: rewritten with load_config() returning a plain dict, DEFAULTS matching openclaw/claude-code fields, ENV_OVERRIDES with typed casting - tools.py: register() uses load_config() instead of raw env vars - __init__.py: clean exports (removed configure/get_config/reset_config) - README.md: full rewrite with config file examples, tables by category - docs/hermes.md: full rewrite with quick start, architecture, all config tables, gateway section, troubleshooting - tests: updated for new config pattern, 46 tests pass * ci: add test job for hermes integration * chore: regenerate docs skill for hermes integration
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
"""Configuration management for Hindsight-Hermes plugin.
|
|
|
|
Loads settings from ``~/.hindsight/hermes.json`` merged with environment
|
|
variable overrides. Follows the same conventions as the openclaw and
|
|
claude-code integrations.
|
|
|
|
Loading order (later entries win):
|
|
1. Built-in defaults
|
|
2. User config (``~/.hindsight/hermes.json``)
|
|
3. Environment variable overrides
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json as _json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
USER_CONFIG_PATH = Path.home() / ".hindsight" / "hermes.json"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Defaults — same field names as openclaw / claude-code settings.json
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DEFAULTS: dict[str, Any] = {
|
|
# Connection
|
|
"hindsightApiUrl": None,
|
|
"hindsightApiToken": None,
|
|
"apiPort": 9077,
|
|
"daemonIdleTimeout": 0,
|
|
"embedVersion": "latest",
|
|
"embedPackagePath": None,
|
|
# Bank
|
|
"bankId": None,
|
|
"bankIdPrefix": "",
|
|
"bankMission": "",
|
|
"retainMission": None,
|
|
# Recall
|
|
"autoRecall": True,
|
|
"recallBudget": "mid",
|
|
"recallMaxTokens": 4096,
|
|
"recallTypes": ["world", "experience"],
|
|
"recallContextTurns": 1,
|
|
"recallMaxQueryChars": 800,
|
|
"recallRoles": ["user", "assistant"],
|
|
"recallPromptPreamble": (
|
|
"Relevant memories from past conversations (prioritize recent when "
|
|
"conflicting). Only use memories that are directly useful to continue "
|
|
"this conversation; ignore the rest:"
|
|
),
|
|
"recallTopK": None,
|
|
# Retain
|
|
"autoRetain": True,
|
|
"retainRoles": ["user", "assistant"],
|
|
"retainEveryNTurns": 1,
|
|
"retainOverlapTurns": 2,
|
|
"retainContext": "hermes",
|
|
# LLM (for daemon mode)
|
|
"llmProvider": None,
|
|
"llmModel": None,
|
|
"llmApiKeyEnv": None,
|
|
# Misc
|
|
"debug": False,
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Env var → config key mapping (same convention as claude-code)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ENV_OVERRIDES: dict[str, tuple[str, type]] = {
|
|
"HINDSIGHT_API_URL": ("hindsightApiUrl", str),
|
|
"HINDSIGHT_API_TOKEN": ("hindsightApiToken", str),
|
|
"HINDSIGHT_API_KEY": ("hindsightApiToken", str), # alias
|
|
"HINDSIGHT_BANK_ID": ("bankId", str),
|
|
"HINDSIGHT_AUTO_RECALL": ("autoRecall", bool),
|
|
"HINDSIGHT_AUTO_RETAIN": ("autoRetain", bool),
|
|
"HINDSIGHT_RECALL_BUDGET": ("recallBudget", str),
|
|
"HINDSIGHT_RECALL_MAX_TOKENS": ("recallMaxTokens", int),
|
|
"HINDSIGHT_RECALL_MAX_QUERY_CHARS": ("recallMaxQueryChars", int),
|
|
"HINDSIGHT_API_PORT": ("apiPort", int),
|
|
"HINDSIGHT_DAEMON_IDLE_TIMEOUT": ("daemonIdleTimeout", int),
|
|
"HINDSIGHT_EMBED_VERSION": ("embedVersion", str),
|
|
"HINDSIGHT_EMBED_PACKAGE_PATH": ("embedPackagePath", str),
|
|
"HINDSIGHT_BANK_MISSION": ("bankMission", str),
|
|
"HINDSIGHT_LLM_PROVIDER": ("llmProvider", str),
|
|
"HINDSIGHT_LLM_MODEL": ("llmModel", str),
|
|
"HINDSIGHT_DEBUG": ("debug", bool),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Loading
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _cast_env(value: str, typ: type) -> Any:
|
|
"""Cast environment variable string to target type. Returns None on failure."""
|
|
try:
|
|
if typ is bool:
|
|
return value.lower() in ("true", "1", "yes")
|
|
if typ is int:
|
|
return int(value)
|
|
return value
|
|
except (ValueError, AttributeError):
|
|
return None
|
|
|
|
|
|
def _load_json_file(path: Path | str) -> dict[str, Any]:
|
|
"""Read a JSON file, returning {} on any error."""
|
|
p = Path(path)
|
|
if not p.exists():
|
|
return {}
|
|
try:
|
|
return _json.loads(p.read_text(encoding="utf-8")) or {}
|
|
except Exception as exc:
|
|
_debug_log(None, f"Failed to read {p}: {exc}")
|
|
return {}
|
|
|
|
|
|
def load_config(config_path: Path | str | None = None) -> dict[str, Any]:
|
|
"""Load plugin configuration.
|
|
|
|
Loading order (later entries win):
|
|
1. Built-in defaults
|
|
2. User config (``~/.hindsight/hermes.json``)
|
|
3. Environment variable overrides
|
|
|
|
Args:
|
|
config_path: Override the user config path (for testing).
|
|
|
|
Returns:
|
|
A plain dict with all configuration values.
|
|
"""
|
|
config = dict(DEFAULTS)
|
|
|
|
# User config — stable, version-independent
|
|
user_path = Path(config_path) if config_path else USER_CONFIG_PATH
|
|
file_cfg = _load_json_file(user_path)
|
|
config.update({k: v for k, v in file_cfg.items() if v is not None})
|
|
|
|
# Environment variable overrides (highest priority)
|
|
for env_name, (key, typ) in ENV_OVERRIDES.items():
|
|
val = os.environ.get(env_name)
|
|
if val is not None:
|
|
cast_val = _cast_env(val, typ)
|
|
if cast_val is not None:
|
|
config[key] = cast_val
|
|
|
|
return config
|
|
|
|
|
|
def write_config(data: dict[str, Any], config_path: Path | str | None = None) -> None:
|
|
"""Write configuration to the user config file."""
|
|
p = Path(config_path) if config_path else USER_CONFIG_PATH
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(
|
|
_json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _debug_log(config: dict | None, *args: Any) -> None:
|
|
"""Log to stderr if debug mode is enabled."""
|
|
if config and config.get("debug"):
|
|
print("[Hindsight]", *args, file=sys.stderr)
|