2026-02-02 19:01:51 -08:00
|
|
|
|
"""
|
|
|
|
|
|
Configuration management for Hermes Agent.
|
|
|
|
|
|
|
|
|
|
|
|
Config files are stored in ~/.hermes/ for easy access:
|
|
|
|
|
|
- ~/.hermes/config.yaml - All settings (model, toolsets, terminal, etc.)
|
|
|
|
|
|
- ~/.hermes/.env - API keys and secrets
|
|
|
|
|
|
|
|
|
|
|
|
This module provides:
|
|
|
|
|
|
- hermes config - Show current configuration
|
|
|
|
|
|
- hermes config edit - Open config in editor
|
|
|
|
|
|
- hermes config set - Set a specific value
|
|
|
|
|
|
- hermes config wizard - Re-run setup wizard
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2026-03-02 22:26:21 -08:00
|
|
|
|
import platform
|
2026-03-13 03:14:04 -07:00
|
|
|
|
import re
|
2026-03-06 15:14:26 +03:00
|
|
|
|
import stat
|
2026-03-13 03:14:04 -07:00
|
|
|
|
import sys
|
2026-02-02 19:01:51 -08:00
|
|
|
|
import subprocess
|
2026-03-06 15:14:26 +03:00
|
|
|
|
import sys
|
2026-03-11 08:58:33 -07:00
|
|
|
|
import tempfile
|
2026-02-02 19:01:51 -08:00
|
|
|
|
from pathlib import Path
|
2026-02-02 19:39:23 -08:00
|
|
|
|
from typing import Dict, Any, Optional, List, Tuple
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-02 22:26:21 -08:00
|
|
|
|
_IS_WINDOWS = platform.system() == "Windows"
|
2026-03-13 03:14:04 -07:00
|
|
|
|
_ENV_VAR_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
2026-03-02 22:26:21 -08:00
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
import yaml
|
|
|
|
|
|
|
2026-02-20 23:23:32 -08:00
|
|
|
|
from hermes_cli.colors import Colors, color
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config paths
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def get_hermes_home() -> Path:
|
|
|
|
|
|
"""Get the Hermes home directory (~/.hermes)."""
|
|
|
|
|
|
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_path() -> Path:
|
|
|
|
|
|
"""Get the main config file path."""
|
|
|
|
|
|
return get_hermes_home() / "config.yaml"
|
|
|
|
|
|
|
|
|
|
|
|
def get_env_path() -> Path:
|
|
|
|
|
|
"""Get the .env file path (for API keys)."""
|
|
|
|
|
|
return get_hermes_home() / ".env"
|
|
|
|
|
|
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
|
|
|
|
"""Get the project installation directory."""
|
|
|
|
|
|
return Path(__file__).parent.parent.resolve()
|
|
|
|
|
|
|
2026-03-09 02:19:32 -07:00
|
|
|
|
def _secure_dir(path):
|
|
|
|
|
|
"""Set directory to owner-only access (0700). No-op on Windows."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.chmod(path, 0o700)
|
|
|
|
|
|
except (OSError, NotImplementedError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _secure_file(path):
|
|
|
|
|
|
"""Set file to owner-only read/write (0600). No-op on Windows."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(str(path)):
|
|
|
|
|
|
os.chmod(path, 0o600)
|
|
|
|
|
|
except (OSError, NotImplementedError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def ensure_hermes_home():
|
2026-03-09 02:19:32 -07:00
|
|
|
|
"""Ensure ~/.hermes directory structure exists with secure permissions."""
|
2026-02-02 19:01:51 -08:00
|
|
|
|
home = get_hermes_home()
|
2026-03-09 02:19:32 -07:00
|
|
|
|
home.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
_secure_dir(home)
|
|
|
|
|
|
for subdir in ("cron", "sessions", "logs", "memories"):
|
|
|
|
|
|
d = home / subdir
|
|
|
|
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
_secure_dir(d)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config loading/saving
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
DEFAULT_CONFIG = {
|
2026-02-08 10:49:24 +00:00
|
|
|
|
"model": "anthropic/claude-opus-4.6",
|
2026-02-02 19:01:51 -08:00
|
|
|
|
"toolsets": ["hermes-cli"],
|
2026-03-07 21:01:23 -08:00
|
|
|
|
"agent": {
|
|
|
|
|
|
"max_turns": 90,
|
|
|
|
|
|
},
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
"terminal": {
|
|
|
|
|
|
"backend": "local",
|
|
|
|
|
|
"cwd": ".", # Use current directory
|
|
|
|
|
|
"timeout": 180,
|
2026-02-02 19:13:41 -08:00
|
|
|
|
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
"singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
"modal_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
2026-03-05 11:12:50 -08:00
|
|
|
|
"daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
|
|
|
|
|
# Container resource limits (docker, singularity, modal, daytona — ignored for local/ssh)
|
2026-03-04 03:29:05 -08:00
|
|
|
|
"container_cpu": 1,
|
|
|
|
|
|
"container_memory": 5120, # MB (default 5GB)
|
|
|
|
|
|
"container_disk": 51200, # MB (default 50GB)
|
|
|
|
|
|
"container_persistent": True, # Persist filesystem across sessions
|
2026-03-09 15:29:34 -07:00
|
|
|
|
# Docker volume mounts — share host directories with the container.
|
|
|
|
|
|
# Each entry is "host_path:container_path" (standard Docker -v syntax).
|
|
|
|
|
|
# Example: ["/home/user/projects:/workspace/projects", "/data:/data"]
|
|
|
|
|
|
"docker_volumes": [],
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"browser": {
|
|
|
|
|
|
"inactivity_timeout": 120,
|
feat: browser console/errors tool, annotated screenshots, auto-recording, and dogfood QA skill
New browser capabilities and a built-in skill for agent-driven web QA.
## New tool: browser_console
Returns console messages (log/warn/error/info) AND uncaught JavaScript
exceptions in a single call. Uses agent-browser's 'console' and 'errors'
commands through the existing session plumbing. Supports --clear to reset
buffers. Verified working in both local and Browserbase cloud modes.
## Enhanced tool: browser_vision(annotate=True)
New boolean parameter on browser_vision. When true, agent-browser overlays
numbered [N] labels on interactive elements — each [N] maps to ref @eN.
Annotation data (element name, role, bounding box) returned alongside the
vision analysis. Useful for QA reports and spatial reasoning.
## Config: browser.record_sessions
Auto-record browser sessions as WebM video files when enabled:
- Starts recording on first browser_navigate
- Stops and saves on browser_close
- Saves to ~/.hermes/browser_recordings/
- Works in both local and cloud modes (verified)
- Disabled by default
## Built-in skill: dogfood
Systematic exploratory QA testing for web applications. Teaches the agent
a 5-phase workflow:
1. Plan — accept URL, create output dirs, set scope
2. Explore — systematic crawl with annotated screenshots
3. Collect Evidence — screenshots, console errors, JS exceptions
4. Categorize — severity (Critical/High/Medium/Low) and category
(Functional/Visual/Accessibility/Console/UX/Content)
5. Report — structured markdown with per-issue evidence
Includes:
- skills/dogfood/SKILL.md — full workflow instructions
- skills/dogfood/references/issue-taxonomy.md — severity/category defs
- skills/dogfood/templates/dogfood-report-template.md — report template
## Tests
21 new tests covering:
- browser_console message/error parsing, clear flag, empty/failed states
- browser_console schema registration
- browser_vision annotate schema and flag passing
- record_sessions config defaults and recording lifecycle
- Dogfood skill file existence and content validation
Addresses #315.
2026-03-08 21:02:14 -07:00
|
|
|
|
"record_sessions": False, # Auto-record browser sessions as WebM videos
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
# Filesystem checkpoints — automatic snapshots before destructive file ops.
|
|
|
|
|
|
# When enabled, the agent takes a snapshot of the working directory once per
|
|
|
|
|
|
# conversation turn (on first write_file/patch call). Use /rollback to restore.
|
|
|
|
|
|
"checkpoints": {
|
|
|
|
|
|
"enabled": False,
|
|
|
|
|
|
"max_snapshots": 50, # Max checkpoints to keep per directory
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
"compression": {
|
|
|
|
|
|
"enabled": True,
|
2026-03-12 15:51:50 -07:00
|
|
|
|
"threshold": 0.50,
|
2026-02-08 10:49:24 +00:00
|
|
|
|
"summary_model": "google/gemini-3-flash-preview",
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"summary_provider": "auto",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-11 20:52:19 -07:00
|
|
|
|
# Auxiliary model config — provider:model for each side task.
|
|
|
|
|
|
# Format: provider is the provider name, model is the model slug.
|
|
|
|
|
|
# "auto" for provider = auto-detect best available provider.
|
|
|
|
|
|
# Empty model = use provider's default auxiliary model.
|
|
|
|
|
|
# All tasks fall back to openrouter:google/gemini-3-flash-preview if
|
|
|
|
|
|
# the configured provider is unavailable.
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"auxiliary": {
|
|
|
|
|
|
"vision": {
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"provider": "auto", # auto | openrouter | nous | codex | custom
|
2026-03-07 08:52:06 -08:00
|
|
|
|
"model": "", # e.g. "google/gemini-2.5-flash", "gpt-4o"
|
|
|
|
|
|
},
|
|
|
|
|
|
"web_extract": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"compression": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"session_search": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"skills_hub": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"mcp": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
"flush_memories": {
|
|
|
|
|
|
"provider": "auto",
|
|
|
|
|
|
"model": "",
|
|
|
|
|
|
},
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"display": {
|
|
|
|
|
|
"compact": False,
|
|
|
|
|
|
"personality": "kawaii",
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
"resume_display": "full",
|
|
|
|
|
|
"bell_on_complete": False,
|
2026-03-11 05:53:21 -07:00
|
|
|
|
"show_reasoning": False,
|
feat: add data-driven skin/theme engine for CLI customization
Adds a skin system that lets users customize the CLI's visual appearance
through data files (YAML) rather than code changes. Skins define: color
palette, spinner faces/verbs/wings, branding text, and tool output prefix.
New files:
- hermes_cli/skin_engine.py — SkinConfig dataclass, built-in skins
(default, ares, mono, slate), YAML loader for user skins from
~/.hermes/skins/, skin management API
- tests/hermes_cli/test_skin_engine.py — 26 tests covering config,
built-in skins, user YAML skins, display integration
Modified files:
- agent/display.py — skin-aware spinner wings, faces, verbs, tool prefix
- hermes_cli/banner.py — skin-aware banner colors (title, border, accent,
dim, text, session) via _skin_color()/_skin_branding() helpers
- cli.py — /skin command handler, skin init from config, skin-aware
response box label and welcome message
- hermes_cli/config.py — add display.skin default
- hermes_cli/commands.py — add /skin to slash commands
Built-in skins:
- default: classic Hermes gold/kawaii
- ares: crimson/bronze war-god theme (from community PRs #579/#725)
- mono: clean grayscale
- slate: cool blue developer theme
User skins: drop a YAML file in ~/.hermes/skins/ with name, colors,
spinner, branding, and tool_prefix fields. Missing values inherit from
the default skin.
2026-03-10 00:37:28 -07:00
|
|
|
|
"skin": "default",
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
2026-02-12 10:05:08 -08:00
|
|
|
|
# Text-to-speech configuration
|
|
|
|
|
|
"tts": {
|
|
|
|
|
|
"provider": "edge", # "edge" (free) | "elevenlabs" (premium) | "openai"
|
|
|
|
|
|
"edge": {
|
|
|
|
|
|
"voice": "en-US-AriaNeural",
|
|
|
|
|
|
# Popular: AriaNeural, JennyNeural, AndrewNeural, BrianNeural, SoniaNeural
|
|
|
|
|
|
},
|
|
|
|
|
|
"elevenlabs": {
|
|
|
|
|
|
"voice_id": "pNInz6obpgDQGcFmaJgB", # Adam
|
|
|
|
|
|
"model_id": "eleven_multilingual_v2",
|
|
|
|
|
|
},
|
|
|
|
|
|
"openai": {
|
|
|
|
|
|
"model": "gpt-4o-mini-tts",
|
|
|
|
|
|
"voice": "alloy",
|
|
|
|
|
|
# Voices: alloy, echo, fable, onyx, nova, shimmer
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"stt": {
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"model": "whisper-1",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"human_delay": {
|
|
|
|
|
|
"mode": "off",
|
|
|
|
|
|
"min_ms": 800,
|
|
|
|
|
|
"max_ms": 2500,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-19 00:57:31 -08:00
|
|
|
|
# Persistent memory -- bounded curated memory injected into system prompt
|
|
|
|
|
|
"memory": {
|
|
|
|
|
|
"memory_enabled": True,
|
|
|
|
|
|
"user_profile_enabled": True,
|
|
|
|
|
|
"memory_char_limit": 2200, # ~800 tokens at 2.75 chars/token
|
|
|
|
|
|
"user_char_limit": 1375, # ~500 tokens at 2.75 chars/token
|
|
|
|
|
|
},
|
feat: configurable subagent provider:model with full credential resolution
Adds delegation.model and delegation.provider config fields so subagents
can run on a completely different provider:model pair than the parent agent.
When delegation.provider is set, the system resolves the full credential
bundle (base_url, api_key, api_mode) via resolve_runtime_provider() —
the same path used by CLI/gateway startup. This means all configured
providers work out of the box: openrouter, nous, zai, kimi-coding,
minimax, minimax-cn.
Key design decisions:
- Provider resolution uses hermes_cli.runtime_provider (single source of
truth for credential resolution across CLI, gateway, cron, and now
delegation)
- When only delegation.model is set (no provider), the model name changes
but parent credentials are inherited (for switching models within the
same provider like OpenRouter)
- When delegation.provider is set, full credentials are resolved
independently — enabling cross-provider delegation (e.g. parent on
Nous Portal, subagents on OpenRouter)
- Clear error messages if provider resolution fails (missing API key,
unknown provider name)
- _load_config() now falls back to hermes_cli.config.load_config() for
gateway/cron contexts where CLI_CONFIG is unavailable
Based on PR #791 by 0xbyt4 (closes #609), reworked to use proper
provider credential resolution instead of passing provider as metadata.
Co-authored-by: 0xbyt4 <0xbyt4@users.noreply.github.com>
2026-03-11 06:12:21 -07:00
|
|
|
|
|
|
|
|
|
|
# Subagent delegation — override the provider:model used by delegate_task
|
|
|
|
|
|
# so child agents can run on a different (cheaper/faster) provider and model.
|
|
|
|
|
|
# Uses the same runtime provider resolution as CLI/gateway startup, so all
|
|
|
|
|
|
# configured providers (OpenRouter, Nous, Z.ai, Kimi, etc.) are supported.
|
|
|
|
|
|
"delegation": {
|
|
|
|
|
|
"model": "", # e.g. "google/gemini-3-flash-preview" (empty = inherit parent model)
|
|
|
|
|
|
"provider": "", # e.g. "openrouter" (empty = inherit parent provider + credentials)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-23 23:55:42 -08:00
|
|
|
|
# Ephemeral prefill messages file — JSON list of {role, content} dicts
|
|
|
|
|
|
# injected at the start of every API call for few-shot priming.
|
|
|
|
|
|
# Never saved to sessions, logs, or trajectories.
|
|
|
|
|
|
"prefill_messages_file": "",
|
|
|
|
|
|
|
2026-02-25 19:34:25 -05:00
|
|
|
|
# Honcho AI-native memory -- reads ~/.honcho/config.json as single source of truth.
|
|
|
|
|
|
# This section is only needed for hermes-specific overrides; everything else
|
|
|
|
|
|
# (apiKey, workspace, peerName, sessions, enabled) comes from the global config.
|
|
|
|
|
|
"honcho": {},
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# IANA timezone (e.g. "Asia/Kolkata", "America/New_York").
|
|
|
|
|
|
# Empty string means use server-local time.
|
|
|
|
|
|
"timezone": "",
|
|
|
|
|
|
|
2026-03-11 09:15:31 -07:00
|
|
|
|
# Discord platform settings (gateway mode)
|
|
|
|
|
|
"discord": {
|
|
|
|
|
|
"require_mention": True, # Require @mention to respond in server channels
|
|
|
|
|
|
"free_response_channels": "", # Comma-separated channel IDs where bot responds without mention
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-02 23:35:18 -08:00
|
|
|
|
# Permanently allowed dangerous command patterns (added via "always" approval)
|
|
|
|
|
|
"command_allowlist": [],
|
2026-03-09 07:38:06 +03:00
|
|
|
|
# User-defined quick commands that bypass the agent loop (type: exec only)
|
|
|
|
|
|
"quick_commands": {},
|
2026-03-09 17:18:09 +03:00
|
|
|
|
# Custom personalities — add your own entries here
|
|
|
|
|
|
# Supports string format: {"name": "system prompt"}
|
|
|
|
|
|
# Or dict format: {"name": {"description": "...", "system_prompt": "...", "tone": "...", "style": "..."}}
|
|
|
|
|
|
"personalities": {},
|
2026-03-03 11:57:18 +05:30
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Config schema version - bump this when adding new required fields
|
2026-03-11 20:52:19 -07:00
|
|
|
|
"_config_version": 7,
|
2026-02-02 19:01:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config Migration System
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2026-03-08 05:55:30 -07:00
|
|
|
|
# Track which env vars were introduced in each config version.
|
|
|
|
|
|
# Migration only mentions vars new since the user's previous version.
|
|
|
|
|
|
ENV_VARS_BY_VERSION: Dict[int, List[str]] = {
|
|
|
|
|
|
3: ["FIRECRAWL_API_KEY", "BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID", "FAL_KEY"],
|
|
|
|
|
|
4: ["VOICE_TOOLS_OPENAI_KEY", "ELEVENLABS_API_KEY"],
|
|
|
|
|
|
5: ["WHATSAPP_ENABLED", "WHATSAPP_MODE", "WHATSAPP_ALLOWED_USERS",
|
|
|
|
|
|
"SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_ALLOWED_USERS"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 23:06:47 +00:00
|
|
|
|
# Required environment variables with metadata for migration prompts.
|
|
|
|
|
|
# LLM provider is required but handled in the setup wizard's provider
|
|
|
|
|
|
# selection step (Nous Portal / OpenRouter / Custom endpoint), so this
|
|
|
|
|
|
# dict is intentionally empty — no single env var is universally required.
|
|
|
|
|
|
REQUIRED_ENV_VARS = {}
|
|
|
|
|
|
|
|
|
|
|
|
# Optional environment variables that enhance functionality
|
|
|
|
|
|
OPTIONAL_ENV_VARS = {
|
2026-02-23 23:25:38 +00:00
|
|
|
|
# ── Provider (handled in provider selection, not shown in checklists) ──
|
2026-03-08 18:40:50 +10:00
|
|
|
|
"NOUS_BASE_URL": {
|
|
|
|
|
|
"description": "Nous Portal base URL override",
|
|
|
|
|
|
"prompt": "Nous Portal base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"OPENROUTER_API_KEY": {
|
2026-02-23 23:06:47 +00:00
|
|
|
|
"description": "OpenRouter API key (for vision, web scraping helpers, and MoA)",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"prompt": "OpenRouter API key",
|
|
|
|
|
|
"url": "https://openrouter.ai/keys",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:06:47 +00:00
|
|
|
|
"tools": ["vision_analyze", "mixture_of_agents"],
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
feat: add z.ai/GLM, Kimi/Moonshot, MiniMax as first-class providers
Adds 4 new direct API-key providers (zai, kimi-coding, minimax, minimax-cn)
to the inference provider system. All use standard OpenAI-compatible
chat/completions endpoints with Bearer token auth.
Core changes:
- auth.py: Extended ProviderConfig with api_key_env_vars and base_url_env_var
fields. Added providers to PROVIDER_REGISTRY. Added provider aliases
(glm, z-ai, zhipu, kimi, moonshot). Added auto-detection of API-key
providers in resolve_provider(). Added resolve_api_key_provider_credentials()
and get_api_key_provider_status() helpers.
- runtime_provider.py: Added generic API-key provider branch in
resolve_runtime_provider() — any provider with auth_type='api_key'
is automatically handled.
- main.py: Added providers to hermes model menu with generic
_model_flow_api_key_provider() flow. Updated _has_any_provider_configured()
to check all provider env vars. Updated argparse --provider choices.
- setup.py: Added providers to setup wizard with API key prompts and
curated model lists.
- config.py: Added env vars (GLM_API_KEY, KIMI_API_KEY, MINIMAX_API_KEY,
etc.) to OPTIONAL_ENV_VARS.
- status.py: Added API key display and provider status section.
- doctor.py: Added connectivity checks for each provider endpoint.
- cli.py: Updated provider docstrings.
Docs: Updated README.md, .env.example, cli-config.yaml.example,
cli-commands.md, environment-variables.md, configuration.md.
Tests: 50 new tests covering registry, aliases, resolution, auto-detection,
credential resolution, and runtime provider dispatch.
Inspired by PR #33 (numman-ali) which proposed a provider registry approach.
Credit to tars90percent (PR #473) and manuelschipper (PR #420) for related
provider improvements merged earlier in this changeset.
2026-03-06 18:55:12 -08:00
|
|
|
|
"GLM_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI / GLM API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"ZAI_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI API key (alias for GLM_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"Z_AI_API_KEY": {
|
|
|
|
|
|
"description": "Z.AI API key (alias for GLM_API_KEY)",
|
|
|
|
|
|
"prompt": "Z.AI API key",
|
|
|
|
|
|
"url": "https://z.ai/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"GLM_BASE_URL": {
|
|
|
|
|
|
"description": "Z.AI / GLM base URL override",
|
|
|
|
|
|
"prompt": "Z.AI / GLM base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"KIMI_API_KEY": {
|
|
|
|
|
|
"description": "Kimi / Moonshot API key",
|
|
|
|
|
|
"prompt": "Kimi API key",
|
|
|
|
|
|
"url": "https://platform.moonshot.cn/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"KIMI_BASE_URL": {
|
|
|
|
|
|
"description": "Kimi / Moonshot base URL override",
|
|
|
|
|
|
"prompt": "Kimi base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_API_KEY": {
|
|
|
|
|
|
"description": "MiniMax API key (international)",
|
|
|
|
|
|
"prompt": "MiniMax API key",
|
|
|
|
|
|
"url": "https://www.minimax.io/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_BASE_URL": {
|
|
|
|
|
|
"description": "MiniMax base URL override",
|
|
|
|
|
|
"prompt": "MiniMax base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_CN_API_KEY": {
|
|
|
|
|
|
"description": "MiniMax API key (China endpoint)",
|
|
|
|
|
|
"prompt": "MiniMax (China) API key",
|
|
|
|
|
|
"url": "https://www.minimaxi.com/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"MINIMAX_CN_BASE_URL": {
|
|
|
|
|
|
"description": "MiniMax (China) base URL override",
|
|
|
|
|
|
"prompt": "MiniMax (China) base URL (leave empty for default)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "provider",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
|
|
|
|
|
# ── Tool API keys ──
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"FIRECRAWL_API_KEY": {
|
|
|
|
|
|
"description": "Firecrawl API key for web search and scraping",
|
|
|
|
|
|
"prompt": "Firecrawl API key",
|
|
|
|
|
|
"url": "https://firecrawl.dev/",
|
|
|
|
|
|
"tools": ["web_search", "web_extract"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-03-05 16:16:18 -06:00
|
|
|
|
"FIRECRAWL_API_URL": {
|
|
|
|
|
|
"description": "Firecrawl API URL for self-hosted instances (optional)",
|
|
|
|
|
|
"prompt": "Firecrawl API URL (leave empty for cloud)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
"advanced": True,
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"BROWSERBASE_API_KEY": {
|
2026-03-07 01:23:27 -08:00
|
|
|
|
"description": "Browserbase API key for cloud browser (optional — local browser works without this)",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Browserbase API key",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"url": "https://browserbase.com/",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"tools": ["browser_navigate", "browser_click"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"BROWSERBASE_PROJECT_ID": {
|
2026-03-07 01:23:27 -08:00
|
|
|
|
"description": "Browserbase project ID (optional — only needed for cloud browser)",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"prompt": "Browserbase project ID",
|
|
|
|
|
|
"url": "https://browserbase.com/",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"tools": ["browser_navigate", "browser_click"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"FAL_KEY": {
|
|
|
|
|
|
"description": "FAL API key for image generation",
|
|
|
|
|
|
"prompt": "FAL API key",
|
|
|
|
|
|
"url": "https://fal.ai/",
|
|
|
|
|
|
"tools": ["image_generate"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-02-04 09:36:51 -08:00
|
|
|
|
"TINKER_API_KEY": {
|
|
|
|
|
|
"description": "Tinker API key for RL training",
|
|
|
|
|
|
"prompt": "Tinker API key",
|
|
|
|
|
|
"url": "https://tinker-console.thinkingmachines.ai/keys",
|
|
|
|
|
|
"tools": ["rl_start_training", "rl_check_status", "rl_stop_training"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-04 09:36:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
"WANDB_API_KEY": {
|
|
|
|
|
|
"description": "Weights & Biases API key for experiment tracking",
|
|
|
|
|
|
"prompt": "WandB API key",
|
|
|
|
|
|
"url": "https://wandb.ai/authorize",
|
|
|
|
|
|
"tools": ["rl_get_results", "rl_check_status"],
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-04 09:36:51 -08:00
|
|
|
|
},
|
2026-02-23 23:21:33 +00:00
|
|
|
|
"VOICE_TOOLS_OPENAI_KEY": {
|
2026-02-17 03:11:17 -08:00
|
|
|
|
"description": "OpenAI API key for voice transcription (Whisper) and OpenAI TTS",
|
|
|
|
|
|
"prompt": "OpenAI API Key (for Whisper STT + TTS)",
|
2026-02-15 21:48:07 -08:00
|
|
|
|
"url": "https://platform.openai.com/api-keys",
|
2026-02-17 03:11:17 -08:00
|
|
|
|
"tools": ["voice_transcription", "openai_tts"],
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"ELEVENLABS_API_KEY": {
|
|
|
|
|
|
"description": "ElevenLabs API key for premium text-to-speech voices",
|
|
|
|
|
|
"prompt": "ElevenLabs API key",
|
|
|
|
|
|
"url": "https://elevenlabs.io/",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"GITHUB_TOKEN": {
|
|
|
|
|
|
"description": "GitHub token for Skills Hub (higher API rate limits, skill publish)",
|
|
|
|
|
|
"prompt": "GitHub Token",
|
|
|
|
|
|
"url": "https://github.com/settings/tokens",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "tool",
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
2026-02-25 19:34:25 -05:00
|
|
|
|
# ── Honcho ──
|
|
|
|
|
|
"HONCHO_API_KEY": {
|
|
|
|
|
|
"description": "Honcho API key for AI-native persistent memory",
|
|
|
|
|
|
"prompt": "Honcho API key",
|
|
|
|
|
|
"url": "https://app.honcho.dev",
|
2026-03-09 17:59:30 -04:00
|
|
|
|
"tools": ["honcho_context"],
|
2026-02-25 19:34:25 -05:00
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "tool",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-23 23:25:38 +00:00
|
|
|
|
# ── Messaging platforms ──
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"TELEGRAM_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Telegram bot token from @BotFather",
|
|
|
|
|
|
"prompt": "Telegram bot token",
|
|
|
|
|
|
"url": "https://t.me/BotFather",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"TELEGRAM_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Telegram user IDs allowed to use the bot (get ID from @userinfobot)",
|
|
|
|
|
|
"prompt": "Allowed Telegram user IDs (comma-separated)",
|
|
|
|
|
|
"url": "https://t.me/userinfobot",
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"DISCORD_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Discord bot token from Developer Portal",
|
|
|
|
|
|
"prompt": "Discord bot token",
|
|
|
|
|
|
"url": "https://discord.com/developers/applications",
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"DISCORD_ALLOWED_USERS": {
|
|
|
|
|
|
"description": "Comma-separated Discord user IDs allowed to use the bot",
|
|
|
|
|
|
"prompt": "Allowed Discord user IDs (comma-separated)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"SLACK_BOT_TOKEN": {
|
2026-03-09 14:00:11 -07:00
|
|
|
|
"description": "Slack bot token (xoxb-). Get from OAuth & Permissions after installing your app. "
|
|
|
|
|
|
"Required scopes: chat:write, app_mentions:read, channels:history, groups:history, "
|
|
|
|
|
|
"im:history, im:read, im:write, users:read, files:write",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Slack Bot Token (xoxb-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"SLACK_APP_TOKEN": {
|
2026-03-09 14:00:11 -07:00
|
|
|
|
"description": "Slack app-level token (xapp-) for Socket Mode. Get from Basic Information → "
|
|
|
|
|
|
"App-Level Tokens. Also ensure Event Subscriptions include: message.im, "
|
|
|
|
|
|
"message.channels, message.groups, app_mention",
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"prompt": "Slack App Token (xapp-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
2026-02-12 10:05:08 -08:00
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
},
|
|
|
|
|
|
"GATEWAY_ALLOW_ALL_USERS": {
|
|
|
|
|
|
"description": "Allow all users to interact with messaging bots (true/false). Default: false.",
|
|
|
|
|
|
"prompt": "Allow all users (true/false)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "messaging",
|
|
|
|
|
|
"advanced": True,
|
2026-02-12 10:05:08 -08:00
|
|
|
|
},
|
2026-02-23 23:25:38 +00:00
|
|
|
|
|
|
|
|
|
|
# ── Agent settings ──
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"MESSAGING_CWD": {
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"description": "Working directory for terminal commands via messaging",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
"prompt": "Messaging working directory (default: home)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
|
|
|
|
|
"SUDO_PASSWORD": {
|
|
|
|
|
|
"description": "Sudo password for terminal commands requiring root access",
|
|
|
|
|
|
"prompt": "Sudo password",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 10:46:23 -08:00
|
|
|
|
},
|
2026-02-03 14:48:19 -08:00
|
|
|
|
"HERMES_MAX_ITERATIONS": {
|
docs: comprehensive AGENTS.md audit and corrections
Major fixes:
- Default model: claude-sonnet-4.6 → claude-opus-4.6
- max_iterations default: 60 → 90 (also fixed in config.py OPTIONAL_ENV_VARS description)
- chat() signature: chat(user_message, task_id) → chat(message)
- Agent loop: _run_agent_loop() doesn't exist, loop is in run_conversation()
- Removed async/await references (agent is entirely synchronous)
- KawaiiSpinner location: run_agent.py → agent/display.py
- NOUS_API_KEY removed (not used by any tool), replaced with VOICE_TOOLS_OPENAI_KEY
- OPENAI_API_KEY for Whisper → VOICE_TOOLS_OPENAI_KEY
- check_for_missing_config() → check_config_version() + get_missing_env_vars()
- Adding tools: '2 files' → '3 files' (tool + model_tools.py + toolsets.py)
- Venv path: venv/ → .venv/
- Trajectory output path: trajectories/*.jsonl → trajectory_samples.jsonl
- process_command() location clarified (HermesCLI in cli.py, not commands.py)
- REQUIRED_ENV_VARS noted as intentionally empty
- _config_version noted as currently at version 5
New content:
- Project structure: added 40+ missing files across agent/, hermes_cli/, tools/, gateway/
- Full gateway/ directory listing with all modules and platforms/
- Added honcho_integration/, scripts/, tests/ directories
- Added hermes_constants.py, hermes_time.py, trajectory_compressor.py, utils.py
- CLI commands table: added 25+ missing commands (model, login, logout, whatsapp,
skills subsystem, tools, insights, gateway start/stop/restart/status/uninstall,
sessions export/delete/prune/stats, config path/env-path/show)
- Gateway slash commands section with all 20+ commands
- Platform toolsets: added hermes-cli, hermes-slack, hermes-homeassistant, hermes-gateway
- Gateway: added Home Assistant as supported platform
2026-03-08 17:38:05 -07:00
|
|
|
|
"description": "Maximum tool-calling iterations per conversation (default: 90)",
|
2026-02-03 14:48:19 -08:00
|
|
|
|
"prompt": "Max iterations",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 14:48:19 -08:00
|
|
|
|
},
|
2026-02-28 00:05:58 -08:00
|
|
|
|
# HERMES_TOOL_PROGRESS and HERMES_TOOL_PROGRESS_MODE are deprecated —
|
|
|
|
|
|
# now configured via display.tool_progress in config.yaml (off|new|all|verbose).
|
|
|
|
|
|
# Gateway falls back to these env vars for backward compatibility.
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"HERMES_TOOL_PROGRESS": {
|
2026-02-28 00:05:58 -08:00
|
|
|
|
"description": "(deprecated) Use display.tool_progress in config.yaml instead",
|
|
|
|
|
|
"prompt": "Tool progress (deprecated — use config.yaml)",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
},
|
|
|
|
|
|
"HERMES_TOOL_PROGRESS_MODE": {
|
2026-02-28 00:05:58 -08:00
|
|
|
|
"description": "(deprecated) Use display.tool_progress in config.yaml instead",
|
|
|
|
|
|
"prompt": "Progress mode (deprecated — use config.yaml)",
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-23 23:25:38 +00:00
|
|
|
|
"category": "setting",
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
},
|
2026-02-23 23:55:42 -08:00
|
|
|
|
"HERMES_PREFILL_MESSAGES_FILE": {
|
|
|
|
|
|
"description": "Path to JSON file with ephemeral prefill messages for few-shot priming",
|
|
|
|
|
|
"prompt": "Prefill messages file path",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "setting",
|
|
|
|
|
|
},
|
|
|
|
|
|
"HERMES_EPHEMERAL_SYSTEM_PROMPT": {
|
|
|
|
|
|
"description": "Ephemeral system prompt injected at API-call time (never persisted to sessions)",
|
|
|
|
|
|
"prompt": "Ephemeral system prompt",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
"category": "setting",
|
|
|
|
|
|
},
|
2026-02-02 19:39:23 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_missing_env_vars(required_only: bool = False) -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check which environment variables are missing.
|
|
|
|
|
|
|
|
|
|
|
|
Returns list of dicts with var info for missing variables.
|
|
|
|
|
|
"""
|
|
|
|
|
|
missing = []
|
|
|
|
|
|
|
|
|
|
|
|
# Check required vars
|
|
|
|
|
|
for var_name, info in REQUIRED_ENV_VARS.items():
|
|
|
|
|
|
if not get_env_value(var_name):
|
|
|
|
|
|
missing.append({"name": var_name, **info, "is_required": True})
|
|
|
|
|
|
|
|
|
|
|
|
# Check optional vars (if not required_only)
|
|
|
|
|
|
if not required_only:
|
|
|
|
|
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
|
|
|
|
|
if not get_env_value(var_name):
|
|
|
|
|
|
missing.append({"name": var_name, **info, "is_required": False})
|
|
|
|
|
|
|
|
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
def _set_nested(config: dict, dotted_key: str, value):
|
|
|
|
|
|
"""Set a value at an arbitrarily nested dotted key path.
|
|
|
|
|
|
|
|
|
|
|
|
Creates intermediate dicts as needed, e.g. ``_set_nested(c, "a.b.c", 1)``
|
|
|
|
|
|
ensures ``c["a"]["b"]["c"] == 1``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
parts = dotted_key.split(".")
|
|
|
|
|
|
current = config
|
|
|
|
|
|
for part in parts[:-1]:
|
|
|
|
|
|
if part not in current or not isinstance(current.get(part), dict):
|
|
|
|
|
|
current[part] = {}
|
|
|
|
|
|
current = current[part]
|
|
|
|
|
|
current[parts[-1]] = value
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
def get_missing_config_fields() -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
Check which config fields are missing or outdated (recursive).
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
Walks the DEFAULT_CONFIG tree at arbitrary depth and reports any keys
|
|
|
|
|
|
present in defaults but absent from the user's loaded config.
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
missing = []
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
|
|
|
|
|
|
def _check(defaults: dict, current: dict, prefix: str = ""):
|
|
|
|
|
|
for key, default_value in defaults.items():
|
|
|
|
|
|
if key.startswith('_'):
|
|
|
|
|
|
continue
|
|
|
|
|
|
full_key = key if not prefix else f"{prefix}.{key}"
|
|
|
|
|
|
if key not in current:
|
|
|
|
|
|
missing.append({
|
|
|
|
|
|
"key": full_key,
|
|
|
|
|
|
"default": default_value,
|
|
|
|
|
|
"description": f"New config option: {full_key}",
|
|
|
|
|
|
})
|
|
|
|
|
|
elif isinstance(default_value, dict) and isinstance(current.get(key), dict):
|
|
|
|
|
|
_check(default_value, current[key], full_key)
|
|
|
|
|
|
|
|
|
|
|
|
_check(DEFAULT_CONFIG, config)
|
2026-02-02 19:39:23 -08:00
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_config_version() -> Tuple[int, int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check config version.
|
|
|
|
|
|
|
|
|
|
|
|
Returns (current_version, latest_version).
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
current = config.get("_config_version", 0)
|
|
|
|
|
|
latest = DEFAULT_CONFIG.get("_config_version", 1)
|
|
|
|
|
|
return current, latest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, Any]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Migrate config to latest version, prompting for new required fields.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
interactive: If True, prompt user for missing values
|
|
|
|
|
|
quiet: If True, suppress output
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict with migration results: {"env_added": [...], "config_added": [...], "warnings": [...]}
|
|
|
|
|
|
"""
|
|
|
|
|
|
results = {"env_added": [], "config_added": [], "warnings": []}
|
|
|
|
|
|
|
|
|
|
|
|
# Check config version
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
|
2026-02-28 00:05:58 -08:00
|
|
|
|
# ── Version 3 → 4: migrate tool progress from .env to config.yaml ──
|
|
|
|
|
|
if current_ver < 4:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
display = config.get("display", {})
|
|
|
|
|
|
if not isinstance(display, dict):
|
|
|
|
|
|
display = {}
|
|
|
|
|
|
if "tool_progress" not in display:
|
|
|
|
|
|
old_enabled = get_env_value("HERMES_TOOL_PROGRESS")
|
|
|
|
|
|
old_mode = get_env_value("HERMES_TOOL_PROGRESS_MODE")
|
|
|
|
|
|
if old_enabled and old_enabled.lower() in ("false", "0", "no"):
|
|
|
|
|
|
display["tool_progress"] = "off"
|
|
|
|
|
|
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
|
|
|
|
|
|
elif old_mode and old_mode.lower() in ("new", "all"):
|
|
|
|
|
|
display["tool_progress"] = old_mode.lower()
|
|
|
|
|
|
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
display["tool_progress"] = "all"
|
|
|
|
|
|
results["config_added"].append("display.tool_progress=all (default)")
|
|
|
|
|
|
config["display"] = display
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
print(f" ✓ Migrated tool progress to config.yaml: {display['tool_progress']}")
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# ── Version 4 → 5: add timezone field ──
|
|
|
|
|
|
if current_ver < 5:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
if "timezone" not in config:
|
2026-03-07 00:05:05 -08:00
|
|
|
|
old_tz = os.getenv("HERMES_TIMEZONE", "")
|
2026-03-03 11:57:18 +05:30
|
|
|
|
if old_tz and old_tz.strip():
|
|
|
|
|
|
config["timezone"] = old_tz.strip()
|
|
|
|
|
|
results["config_added"].append(f"timezone={old_tz.strip()} (from HERMES_TIMEZONE)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
config["timezone"] = ""
|
|
|
|
|
|
results["config_added"].append("timezone= (empty, uses server-local)")
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
tz_display = config["timezone"] or "(server-local)"
|
|
|
|
|
|
print(f" ✓ Added timezone to config.yaml: {tz_display}")
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
if current_ver < latest_ver and not quiet:
|
|
|
|
|
|
print(f"Config version: {current_ver} → {latest_ver}")
|
|
|
|
|
|
|
|
|
|
|
|
# Check for missing required env vars
|
|
|
|
|
|
missing_env = get_missing_env_vars(required_only=True)
|
|
|
|
|
|
|
|
|
|
|
|
if missing_env and not quiet:
|
|
|
|
|
|
print("\n⚠️ Missing required environment variables:")
|
|
|
|
|
|
for var in missing_env:
|
|
|
|
|
|
print(f" • {var['name']}: {var['description']}")
|
|
|
|
|
|
|
|
|
|
|
|
if interactive and missing_env:
|
|
|
|
|
|
print("\nLet's configure them now:\n")
|
|
|
|
|
|
for var in missing_env:
|
|
|
|
|
|
if var.get("url"):
|
|
|
|
|
|
print(f" Get your key at: {var['url']}")
|
|
|
|
|
|
|
|
|
|
|
|
if var.get("password"):
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
value = getpass.getpass(f" {var['prompt']}: ")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = input(f" {var['prompt']}: ").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if value:
|
|
|
|
|
|
save_env_value(var["name"], value)
|
|
|
|
|
|
results["env_added"].append(var["name"])
|
|
|
|
|
|
print(f" ✓ Saved {var['name']}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
results["warnings"].append(f"Skipped {var['name']} - some features may not work")
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
2026-02-15 21:53:59 -08:00
|
|
|
|
# Check for missing optional env vars and offer to configure interactively
|
|
|
|
|
|
# Skip "advanced" vars (like OPENAI_BASE_URL) -- those are for power users
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
missing_optional = get_missing_env_vars(required_only=False)
|
|
|
|
|
|
required_names = {v["name"] for v in missing_env} if missing_env else set()
|
2026-02-15 21:53:59 -08:00
|
|
|
|
missing_optional = [
|
|
|
|
|
|
v for v in missing_optional
|
|
|
|
|
|
if v["name"] not in required_names and not v.get("advanced")
|
|
|
|
|
|
]
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
|
2026-03-08 05:55:30 -07:00
|
|
|
|
# Only offer to configure env vars that are NEW since the user's previous version
|
|
|
|
|
|
new_var_names = set()
|
|
|
|
|
|
for ver in range(current_ver + 1, latest_ver + 1):
|
|
|
|
|
|
new_var_names.update(ENV_VARS_BY_VERSION.get(ver, []))
|
|
|
|
|
|
|
|
|
|
|
|
if new_var_names and interactive and not quiet:
|
|
|
|
|
|
new_and_unset = [
|
|
|
|
|
|
(name, OPTIONAL_ENV_VARS[name])
|
|
|
|
|
|
for name in sorted(new_var_names)
|
|
|
|
|
|
if not get_env_value(name) and name in OPTIONAL_ENV_VARS
|
|
|
|
|
|
]
|
|
|
|
|
|
if new_and_unset:
|
|
|
|
|
|
print(f"\n {len(new_and_unset)} new optional key(s) in this update:")
|
|
|
|
|
|
for name, info in new_and_unset:
|
|
|
|
|
|
print(f" • {name} — {info.get('description', '')}")
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
print()
|
2026-03-08 05:55:30 -07:00
|
|
|
|
try:
|
|
|
|
|
|
answer = input(" Configure new keys? [y/N]: ").strip().lower()
|
|
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
|
|
|
|
answer = "n"
|
|
|
|
|
|
|
|
|
|
|
|
if answer in ("y", "yes"):
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
print()
|
2026-03-08 05:55:30 -07:00
|
|
|
|
for name, info in new_and_unset:
|
|
|
|
|
|
if info.get("url"):
|
|
|
|
|
|
print(f" {info.get('description', name)}")
|
|
|
|
|
|
print(f" Get your key at: {info['url']}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" {info.get('description', name)}")
|
|
|
|
|
|
if info.get("password"):
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
value = getpass.getpass(f" {info.get('prompt', name)} (Enter to skip): ")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = input(f" {info.get('prompt', name)} (Enter to skip): ").strip()
|
|
|
|
|
|
if value:
|
|
|
|
|
|
save_env_value(name, value)
|
|
|
|
|
|
results["env_added"].append(name)
|
|
|
|
|
|
print(f" ✓ Saved {name}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" Set later with: hermes config set KEY VALUE")
|
Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks
Major feature additions inspired by OpenClaw/ClawdBot integration analysis:
Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)
Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description
Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling
Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads
DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending
Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)
Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications
Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings
Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style
Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
2026-02-15 21:38:59 -08:00
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Check for missing config fields
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
|
|
|
|
|
|
for field in missing_config:
|
|
|
|
|
|
key = field["key"]
|
|
|
|
|
|
default = field["default"]
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
_set_nested(config, key, default)
|
2026-02-02 19:39:23 -08:00
|
|
|
|
results["config_added"].append(key)
|
|
|
|
|
|
if not quiet:
|
|
|
|
|
|
print(f" ✓ Added {key} = {default}")
|
|
|
|
|
|
|
|
|
|
|
|
# Update version and save
|
|
|
|
|
|
config["_config_version"] = latest_ver
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
elif current_ver < latest_ver:
|
|
|
|
|
|
# Just update version
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
config["_config_version"] = latest_ver
|
|
|
|
|
|
save_config(config)
|
|
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
|
|
|
|
"""Recursively merge *override* into *base*, preserving nested defaults.
|
|
|
|
|
|
|
|
|
|
|
|
Keys in *override* take precedence. If both values are dicts the merge
|
|
|
|
|
|
recurses, so a user who overrides only ``tts.elevenlabs.voice_id`` will
|
|
|
|
|
|
keep the default ``tts.elevenlabs.model_id`` intact.
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = base.copy()
|
|
|
|
|
|
for key, value in override.items():
|
|
|
|
|
|
if (
|
|
|
|
|
|
key in result
|
|
|
|
|
|
and isinstance(result[key], dict)
|
|
|
|
|
|
and isinstance(value, dict)
|
|
|
|
|
|
):
|
|
|
|
|
|
result[key] = _deep_merge(result[key], value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
result[key] = value
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
def _normalize_max_turns_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
"""Normalize legacy root-level max_turns into agent.max_turns."""
|
|
|
|
|
|
config = dict(config)
|
|
|
|
|
|
agent_config = dict(config.get("agent") or {})
|
|
|
|
|
|
|
|
|
|
|
|
if "max_turns" in config and "max_turns" not in agent_config:
|
|
|
|
|
|
agent_config["max_turns"] = config["max_turns"]
|
|
|
|
|
|
|
|
|
|
|
|
if "max_turns" not in agent_config:
|
|
|
|
|
|
agent_config["max_turns"] = DEFAULT_CONFIG["agent"]["max_turns"]
|
|
|
|
|
|
|
|
|
|
|
|
config["agent"] = agent_config
|
|
|
|
|
|
config.pop("max_turns", None)
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def load_config() -> Dict[str, Any]:
|
|
|
|
|
|
"""Load configuration from ~/.hermes/config.yaml."""
|
2026-02-16 00:33:45 -08:00
|
|
|
|
import copy
|
2026-02-02 19:01:51 -08:00
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
if config_path.exists():
|
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, encoding="utf-8") as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
user_config = yaml.safe_load(f) or {}
|
2026-03-05 17:04:33 -05:00
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
if "max_turns" in user_config:
|
|
|
|
|
|
agent_user_config = dict(user_config.get("agent") or {})
|
|
|
|
|
|
if agent_user_config.get("max_turns") is None:
|
|
|
|
|
|
agent_user_config["max_turns"] = user_config["max_turns"]
|
|
|
|
|
|
user_config["agent"] = agent_user_config
|
|
|
|
|
|
user_config.pop("max_turns", None)
|
|
|
|
|
|
|
refactor: deduplicate toolsets, unify async bridging, fix approval race condition, harden security
- Replace 4 copy-pasted messaging platform toolsets with shared _HERMES_CORE_TOOLS list
- Consolidate 5 ad-hoc async-bridging patterns into single _run_async() in model_tools.py
- Removes deprecated get_event_loop()/set_event_loop() calls
- Makes all tool handlers self-protecting regardless of caller's event loop state
- RL handler refactored from if/elif chain to dispatch dict
- Fix exec approval race condition: replace module-level globals with thread-safe
per-session tools/approval.py (submit_pending, pop_pending, approve_session, is_approved)
- Session A approving "rm" no longer approves it for all other sessions
- Fix config deep merge: user overriding tts.elevenlabs.voice_id no longer clobbers
tts.elevenlabs.model_id; migration detection now recurses to arbitrary depth
- Gateway default-deny: unauthenticated users denied unless GATEWAY_ALLOW_ALL_USERS=true
- Add 10 dangerous command patterns: rm --recursive, bash -c, python -e, curl|bash,
xargs rm, find -delete
- Sanitize gateway error messages: users see generic message, full traceback goes to logs
2026-02-21 18:28:49 -08:00
|
|
|
|
config = _deep_merge(config, user_config)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Warning: Failed to load config: {e}")
|
|
|
|
|
|
|
2026-03-07 21:01:23 -08:00
|
|
|
|
return _normalize_max_turns_config(config)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
feat(honcho): async memory integration with prefetch pipeline and recallMode
Adds full Honcho memory integration to Hermes:
- Session manager with async background writes, memory modes (honcho/hybrid/local),
and dialectic prefetch for first-turn context warming
- Agent integration: prefetch pipeline, tool surface gated by recallMode,
system prompt context injection, SIGTERM/SIGINT flush handlers
- CLI commands: setup, status, mode, tokens, peer, identity, migrate
- recallMode setting (auto | context | tools) for A/B testing retrieval strategies
- Session strategies: per-session, per-repo (git tree root), per-directory, global
- Polymorphic memoryMode config: string shorthand or per-peer object overrides
- 97 tests covering async writes, client config, session resolution, and memory modes
2026-03-09 15:58:22 -04:00
|
|
|
|
_COMMENTED_SECTIONS = """
|
|
|
|
|
|
# ── Security ──────────────────────────────────────────────────────────
|
|
|
|
|
|
# API keys, tokens, and passwords are redacted from tool output by default.
|
|
|
|
|
|
# Set to false to see full values (useful for debugging auth issues).
|
|
|
|
|
|
#
|
|
|
|
|
|
# security:
|
|
|
|
|
|
# redact_secrets: false
|
|
|
|
|
|
|
|
|
|
|
|
# ── Fallback Model ────────────────────────────────────────────────────
|
|
|
|
|
|
# Automatic provider failover when primary is unavailable.
|
|
|
|
|
|
# Uncomment and configure to enable. Triggers on rate limits (429),
|
|
|
|
|
|
# overload (529), service errors (503), or connection failures.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Supported providers:
|
|
|
|
|
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
|
|
|
|
|
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
|
|
|
|
|
# nous (OAuth — hermes login) — Nous Portal
|
|
|
|
|
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
|
|
|
|
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
|
|
|
|
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
|
|
|
|
|
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
|
|
|
|
|
|
#
|
|
|
|
|
|
# For custom OpenAI-compatible endpoints, add base_url and api_key_env.
|
|
|
|
|
|
#
|
|
|
|
|
|
# fallback_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: anthropic/claude-sonnet-4
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 01:12:49 -07:00
|
|
|
|
_COMMENTED_SECTIONS = """
|
|
|
|
|
|
# ── Security ──────────────────────────────────────────────────────────
|
|
|
|
|
|
# API keys, tokens, and passwords are redacted from tool output by default.
|
|
|
|
|
|
# Set to false to see full values (useful for debugging auth issues).
|
|
|
|
|
|
#
|
|
|
|
|
|
# security:
|
|
|
|
|
|
# redact_secrets: false
|
|
|
|
|
|
|
|
|
|
|
|
# ── Fallback Model ────────────────────────────────────────────────────
|
|
|
|
|
|
# Automatic provider failover when primary is unavailable.
|
2026-03-08 21:25:58 -07:00
|
|
|
|
# Uncomment and configure to enable. Triggers on rate limits (429),
|
|
|
|
|
|
# overload (529), service errors (503), or connection failures.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Supported providers:
|
|
|
|
|
|
# openrouter (OPENROUTER_API_KEY) — routes to any model
|
2026-03-08 21:34:15 -07:00
|
|
|
|
# openai-codex (OAuth — hermes login) — OpenAI Codex
|
refactor: unified OAuth/API-key credential resolution for fallback
Split fallback provider handling into two clean registries:
_FALLBACK_API_KEY_PROVIDERS — env-var-based (openrouter, zai, kimi, minimax)
_FALLBACK_OAUTH_PROVIDERS — OAuth-based (openai-codex, nous)
New _resolve_fallback_credentials() method handles all three cases
(OAuth, API key, custom endpoint) and returns a uniform (key, url, mode)
tuple. _try_activate_fallback() is now just validation + client build.
Adds Nous Portal as a fallback provider — uses the same OAuth flow
as the primary provider (hermes login), returns chat_completions mode.
OAuth providers get credential refresh for free: the existing 401
retry handlers (_try_refresh_codex/nous_client_credentials) check
self.provider, which is set correctly after fallback activation.
4 new tests (nous activation, nous no-login, codex retained).
27 total fallback tests passing, 2548 full suite.
2026-03-08 21:44:48 -07:00
|
|
|
|
# nous (OAuth — hermes login) — Nous Portal
|
2026-03-08 21:25:58 -07:00
|
|
|
|
# zai (ZAI_API_KEY) — Z.AI / GLM
|
|
|
|
|
|
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
|
|
|
|
|
|
# minimax (MINIMAX_API_KEY) — MiniMax
|
|
|
|
|
|
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
|
|
|
|
|
|
#
|
|
|
|
|
|
# For custom OpenAI-compatible endpoints, add base_url and api_key_env.
|
|
|
|
|
|
#
|
|
|
|
|
|
# fallback_model:
|
|
|
|
|
|
# provider: openrouter
|
|
|
|
|
|
# model: anthropic/claude-sonnet-4
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def save_config(config: Dict[str, Any]):
|
|
|
|
|
|
"""Save configuration to ~/.hermes/config.yaml."""
|
2026-03-08 18:55:09 +03:30
|
|
|
|
from utils import atomic_yaml_write
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
config_path = get_config_path()
|
2026-03-07 21:01:23 -08:00
|
|
|
|
normalized = _normalize_max_turns_config(config)
|
2026-03-08 18:55:09 +03:30
|
|
|
|
|
|
|
|
|
|
# Build optional commented-out sections for features that are off by
|
|
|
|
|
|
# default or only relevant when explicitly configured.
|
|
|
|
|
|
sections = []
|
|
|
|
|
|
sec = normalized.get("security", {})
|
|
|
|
|
|
if not sec or sec.get("redact_secrets") is None:
|
|
|
|
|
|
sections.append("security")
|
|
|
|
|
|
fb = normalized.get("fallback_model", {})
|
|
|
|
|
|
if not fb or not (fb.get("provider") and fb.get("model")):
|
|
|
|
|
|
sections.append("fallback")
|
|
|
|
|
|
|
|
|
|
|
|
atomic_yaml_write(
|
|
|
|
|
|
config_path,
|
|
|
|
|
|
normalized,
|
|
|
|
|
|
extra_content=_COMMENTED_SECTIONS if sections else None,
|
|
|
|
|
|
)
|
2026-03-09 02:19:32 -07:00
|
|
|
|
_secure_file(config_path)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_env() -> Dict[str, str]:
|
|
|
|
|
|
"""Load environment variables from ~/.hermes/.env."""
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
env_vars = {}
|
|
|
|
|
|
|
|
|
|
|
|
if env_path.exists():
|
2026-03-02 22:26:21 -08:00
|
|
|
|
# On Windows, open() defaults to the system locale (cp1252) which can
|
|
|
|
|
|
# fail on UTF-8 .env files. Use explicit UTF-8 only on Windows.
|
|
|
|
|
|
open_kw = {"encoding": "utf-8", "errors": "replace"} if _IS_WINDOWS else {}
|
|
|
|
|
|
with open(env_path, **open_kw) as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
for line in f:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if line and not line.startswith('#') and '=' in line:
|
|
|
|
|
|
key, _, value = line.partition('=')
|
|
|
|
|
|
env_vars[key.strip()] = value.strip().strip('"\'')
|
|
|
|
|
|
|
|
|
|
|
|
return env_vars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_env_value(key: str, value: str):
|
|
|
|
|
|
"""Save or update a value in ~/.hermes/.env."""
|
2026-03-13 03:14:04 -07:00
|
|
|
|
if not _ENV_VAR_NAME_RE.match(key):
|
|
|
|
|
|
raise ValueError(f"Invalid environment variable name: {key!r}")
|
|
|
|
|
|
value = value.replace("\n", "").replace("\r", "")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
|
2026-03-02 22:26:21 -08:00
|
|
|
|
# On Windows, open() defaults to the system locale (cp1252) which can
|
|
|
|
|
|
# cause OSError errno 22 on UTF-8 .env files.
|
|
|
|
|
|
read_kw = {"encoding": "utf-8", "errors": "replace"} if _IS_WINDOWS else {}
|
|
|
|
|
|
write_kw = {"encoding": "utf-8"} if _IS_WINDOWS else {}
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines = []
|
|
|
|
|
|
if env_path.exists():
|
2026-03-02 22:26:21 -08:00
|
|
|
|
with open(env_path, **read_kw) as f:
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines = f.readlines()
|
|
|
|
|
|
|
|
|
|
|
|
# Find and update or append
|
|
|
|
|
|
found = False
|
|
|
|
|
|
for i, line in enumerate(lines):
|
|
|
|
|
|
if line.strip().startswith(f"{key}="):
|
|
|
|
|
|
lines[i] = f"{key}={value}\n"
|
|
|
|
|
|
found = True
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not found:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Ensure there's a newline at the end of the file before appending
|
|
|
|
|
|
if lines and not lines[-1].endswith("\n"):
|
|
|
|
|
|
lines[-1] += "\n"
|
2026-02-02 19:01:51 -08:00
|
|
|
|
lines.append(f"{key}={value}\n")
|
|
|
|
|
|
|
2026-03-11 08:58:33 -07:00
|
|
|
|
fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_')
|
|
|
|
|
|
try:
|
|
|
|
|
|
with os.fdopen(fd, 'w', **write_kw) as f:
|
|
|
|
|
|
f.writelines(lines)
|
|
|
|
|
|
f.flush()
|
|
|
|
|
|
os.fsync(f.fileno())
|
|
|
|
|
|
os.replace(tmp_path, env_path)
|
|
|
|
|
|
except BaseException:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
raise
|
2026-03-09 02:19:32 -07:00
|
|
|
|
_secure_file(env_path)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-13 03:14:04 -07:00
|
|
|
|
os.environ[key] = value
|
|
|
|
|
|
|
2026-03-06 15:14:26 +03:00
|
|
|
|
# Restrict .env permissions to owner-only (contains API keys)
|
|
|
|
|
|
if not _IS_WINDOWS:
|
|
|
|
|
|
try:
|
|
|
|
|
|
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-03-13 02:09:52 -07:00
|
|
|
|
def save_anthropic_oauth_token(value: str, save_fn=None):
|
|
|
|
|
|
"""Persist an Anthropic OAuth/setup token and clear the API-key slot."""
|
|
|
|
|
|
writer = save_fn or save_env_value
|
|
|
|
|
|
writer("ANTHROPIC_TOKEN", value)
|
|
|
|
|
|
writer("ANTHROPIC_API_KEY", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_anthropic_api_key(value: str, save_fn=None):
|
|
|
|
|
|
"""Persist an Anthropic API key and clear the OAuth/setup-token slot."""
|
|
|
|
|
|
writer = save_fn or save_env_value
|
|
|
|
|
|
writer("ANTHROPIC_API_KEY", value)
|
|
|
|
|
|
writer("ANTHROPIC_TOKEN", "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-13 03:14:04 -07:00
|
|
|
|
def save_env_value_secure(key: str, value: str) -> Dict[str, Any]:
|
|
|
|
|
|
save_env_value(key, value)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"stored_as": key,
|
|
|
|
|
|
"validated": False,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
def get_env_value(key: str) -> Optional[str]:
|
|
|
|
|
|
"""Get a value from ~/.hermes/.env or environment."""
|
|
|
|
|
|
# Check environment first
|
|
|
|
|
|
if key in os.environ:
|
|
|
|
|
|
return os.environ[key]
|
|
|
|
|
|
|
|
|
|
|
|
# Then check .env file
|
|
|
|
|
|
env_vars = load_env()
|
|
|
|
|
|
return env_vars.get(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config display
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def redact_key(key: str) -> str:
|
|
|
|
|
|
"""Redact an API key for display."""
|
|
|
|
|
|
if not key:
|
|
|
|
|
|
return color("(not set)", Colors.DIM)
|
|
|
|
|
|
if len(key) < 12:
|
|
|
|
|
|
return "***"
|
|
|
|
|
|
return key[:4] + "..." + key[-4:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def show_config():
|
|
|
|
|
|
"""Display current configuration."""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
|
2026-02-20 21:25:04 -08:00
|
|
|
|
print(color("│ ⚕ Hermes Configuration │", Colors.CYAN))
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN))
|
|
|
|
|
|
|
|
|
|
|
|
# Paths
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Paths", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print(f" Config: {get_config_path()}")
|
|
|
|
|
|
print(f" Secrets: {get_env_path()}")
|
|
|
|
|
|
print(f" Install: {get_project_root()}")
|
|
|
|
|
|
|
|
|
|
|
|
# API Keys
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ API Keys", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
|
|
keys = [
|
|
|
|
|
|
("OPENROUTER_API_KEY", "OpenRouter"),
|
2026-02-23 23:21:33 +00:00
|
|
|
|
("VOICE_TOOLS_OPENAI_KEY", "OpenAI (STT/TTS)"),
|
2026-02-02 19:01:51 -08:00
|
|
|
|
("FIRECRAWL_API_KEY", "Firecrawl"),
|
|
|
|
|
|
("BROWSERBASE_API_KEY", "Browserbase"),
|
|
|
|
|
|
("FAL_KEY", "FAL"),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for env_key, name in keys:
|
|
|
|
|
|
value = get_env_value(env_key)
|
|
|
|
|
|
print(f" {name:<14} {redact_key(value)}")
|
2026-03-13 02:09:52 -07:00
|
|
|
|
anthropic_value = get_env_value("ANTHROPIC_TOKEN") or get_env_value("ANTHROPIC_API_KEY")
|
|
|
|
|
|
print(f" {'Anthropic':<14} {redact_key(anthropic_value)}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
# Model settings
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Model", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print(f" Model: {config.get('model', 'not set')}")
|
2026-03-07 21:01:23 -08:00
|
|
|
|
print(f" Max turns: {config.get('agent', {}).get('max_turns', DEFAULT_CONFIG['agent']['max_turns'])}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(f" Toolsets: {', '.join(config.get('toolsets', ['all']))}")
|
|
|
|
|
|
|
2026-03-11 05:53:21 -07:00
|
|
|
|
# Display
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Display", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
display = config.get('display', {})
|
|
|
|
|
|
print(f" Personality: {display.get('personality', 'kawaii')}")
|
|
|
|
|
|
print(f" Reasoning: {'on' if display.get('show_reasoning', False) else 'off'}")
|
|
|
|
|
|
print(f" Bell: {'on' if display.get('bell_on_complete', False) else 'off'}")
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# Terminal
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Terminal", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
terminal = config.get('terminal', {})
|
|
|
|
|
|
print(f" Backend: {terminal.get('backend', 'local')}")
|
|
|
|
|
|
print(f" Working dir: {terminal.get('cwd', '.')}")
|
|
|
|
|
|
print(f" Timeout: {terminal.get('timeout', 60)}s")
|
|
|
|
|
|
|
|
|
|
|
|
if terminal.get('backend') == 'docker':
|
|
|
|
|
|
print(f" Docker image: {terminal.get('docker_image', 'python:3.11-slim')}")
|
2026-02-02 19:13:41 -08:00
|
|
|
|
elif terminal.get('backend') == 'singularity':
|
|
|
|
|
|
print(f" Image: {terminal.get('singularity_image', 'docker://python:3.11')}")
|
|
|
|
|
|
elif terminal.get('backend') == 'modal':
|
|
|
|
|
|
print(f" Modal image: {terminal.get('modal_image', 'python:3.11')}")
|
|
|
|
|
|
modal_token = get_env_value('MODAL_TOKEN_ID')
|
|
|
|
|
|
print(f" Modal token: {'configured' if modal_token else '(not set)'}")
|
2026-03-05 11:12:50 -08:00
|
|
|
|
elif terminal.get('backend') == 'daytona':
|
|
|
|
|
|
print(f" Daytona image: {terminal.get('daytona_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
|
|
|
|
|
daytona_key = get_env_value('DAYTONA_API_KEY')
|
|
|
|
|
|
print(f" API key: {'configured' if daytona_key else '(not set)'}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
elif terminal.get('backend') == 'ssh':
|
|
|
|
|
|
ssh_host = get_env_value('TERMINAL_SSH_HOST')
|
|
|
|
|
|
ssh_user = get_env_value('TERMINAL_SSH_USER')
|
|
|
|
|
|
print(f" SSH host: {ssh_host or '(not set)'}")
|
|
|
|
|
|
print(f" SSH user: {ssh_user or '(not set)'}")
|
|
|
|
|
|
|
2026-03-03 11:57:18 +05:30
|
|
|
|
# Timezone
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Timezone", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
tz = config.get('timezone', '')
|
|
|
|
|
|
if tz:
|
|
|
|
|
|
print(f" Timezone: {tz}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" Timezone: {color('(server-local)', Colors.DIM)}")
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
# Compression
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Context Compression", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
compression = config.get('compression', {})
|
|
|
|
|
|
enabled = compression.get('enabled', True)
|
|
|
|
|
|
print(f" Enabled: {'yes' if enabled else 'no'}")
|
|
|
|
|
|
if enabled:
|
2026-03-12 15:51:50 -07:00
|
|
|
|
print(f" Threshold: {compression.get('threshold', 0.50) * 100:.0f}%")
|
2026-02-08 10:49:24 +00:00
|
|
|
|
print(f" Model: {compression.get('summary_model', 'google/gemini-3-flash-preview')}")
|
2026-03-07 08:52:06 -08:00
|
|
|
|
comp_provider = compression.get('summary_provider', 'auto')
|
|
|
|
|
|
if comp_provider != 'auto':
|
|
|
|
|
|
print(f" Provider: {comp_provider}")
|
|
|
|
|
|
|
|
|
|
|
|
# Auxiliary models
|
|
|
|
|
|
auxiliary = config.get('auxiliary', {})
|
|
|
|
|
|
aux_tasks = {
|
|
|
|
|
|
"Vision": auxiliary.get('vision', {}),
|
|
|
|
|
|
"Web extract": auxiliary.get('web_extract', {}),
|
|
|
|
|
|
}
|
|
|
|
|
|
has_overrides = any(
|
|
|
|
|
|
t.get('provider', 'auto') != 'auto' or t.get('model', '')
|
|
|
|
|
|
for t in aux_tasks.values()
|
|
|
|
|
|
)
|
|
|
|
|
|
if has_overrides:
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Auxiliary Models (overrides)", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
for label, task_cfg in aux_tasks.items():
|
|
|
|
|
|
prov = task_cfg.get('provider', 'auto')
|
|
|
|
|
|
mdl = task_cfg.get('model', '')
|
|
|
|
|
|
if prov != 'auto' or mdl:
|
|
|
|
|
|
parts = [f"provider={prov}"]
|
|
|
|
|
|
if mdl:
|
|
|
|
|
|
parts.append(f"model={mdl}")
|
|
|
|
|
|
print(f" {label:12s} {', '.join(parts)}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
# Messaging
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Messaging Platforms", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
|
|
|
|
|
|
telegram_token = get_env_value('TELEGRAM_BOT_TOKEN')
|
|
|
|
|
|
discord_token = get_env_value('DISCORD_BOT_TOKEN')
|
|
|
|
|
|
|
|
|
|
|
|
print(f" Telegram: {'configured' if telegram_token else color('not configured', Colors.DIM)}")
|
|
|
|
|
|
print(f" Discord: {'configured' if discord_token else color('not configured', Colors.DIM)}")
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("─" * 60, Colors.DIM))
|
|
|
|
|
|
print(color(" hermes config edit # Edit config file", Colors.DIM))
|
|
|
|
|
|
print(color(" hermes config set KEY VALUE", Colors.DIM))
|
|
|
|
|
|
print(color(" hermes setup # Run setup wizard", Colors.DIM))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def edit_config():
|
|
|
|
|
|
"""Open config file in user's editor."""
|
|
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
|
|
|
|
|
|
# Ensure config exists
|
|
|
|
|
|
if not config_path.exists():
|
|
|
|
|
|
save_config(DEFAULT_CONFIG)
|
|
|
|
|
|
print(f"Created {config_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Find editor
|
|
|
|
|
|
editor = os.getenv('EDITOR') or os.getenv('VISUAL')
|
|
|
|
|
|
|
|
|
|
|
|
if not editor:
|
|
|
|
|
|
# Try common editors
|
|
|
|
|
|
for cmd in ['nano', 'vim', 'vi', 'code', 'notepad']:
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
if shutil.which(cmd):
|
|
|
|
|
|
editor = cmd
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not editor:
|
2026-03-13 03:14:04 -07:00
|
|
|
|
print("No editor found. Config file is at:")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
print(f" {config_path}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Opening {config_path} in {editor}...")
|
|
|
|
|
|
subprocess.run([editor, str(config_path)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_config_value(key: str, value: str):
|
|
|
|
|
|
"""Set a configuration value."""
|
|
|
|
|
|
# Check if it's an API key (goes to .env)
|
|
|
|
|
|
api_keys = [
|
2026-03-06 08:45:35 +01:00
|
|
|
|
'OPENROUTER_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'VOICE_TOOLS_OPENAI_KEY',
|
2026-03-05 16:16:18 -06:00
|
|
|
|
'FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
'FAL_KEY', 'TELEGRAM_BOT_TOKEN', 'DISCORD_BOT_TOKEN',
|
|
|
|
|
|
'TERMINAL_SSH_HOST', 'TERMINAL_SSH_USER', 'TERMINAL_SSH_KEY',
|
2026-02-16 00:33:45 -08:00
|
|
|
|
'SUDO_PASSWORD', 'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN',
|
2026-03-08 17:45:38 -07:00
|
|
|
|
'GITHUB_TOKEN', 'HONCHO_API_KEY', 'WANDB_API_KEY',
|
2026-03-06 08:45:35 +01:00
|
|
|
|
'TINKER_API_KEY',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
2026-03-06 08:45:35 +01:00
|
|
|
|
if key.upper() in api_keys or key.upper().endswith('_API_KEY') or key.upper().endswith('_TOKEN') or key.upper().startswith('TERMINAL_SSH'):
|
2026-02-02 19:01:51 -08:00
|
|
|
|
save_env_value(key.upper(), value)
|
|
|
|
|
|
print(f"✓ Set {key} in {get_env_path()}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Otherwise it goes to config.yaml
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Read the raw user config (not merged with defaults) to avoid
|
|
|
|
|
|
# dumping all default values back to the file
|
|
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
user_config = {}
|
|
|
|
|
|
if config_path.exists():
|
|
|
|
|
|
try:
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, encoding="utf-8") as f:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
user_config = yaml.safe_load(f) or {}
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
user_config = {}
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Handle nested keys (e.g., "tts.provider")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
parts = key.split('.')
|
2026-02-16 00:33:45 -08:00
|
|
|
|
current = user_config
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
for part in parts[:-1]:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
if part not in current or not isinstance(current.get(part), dict):
|
2026-02-02 19:01:51 -08:00
|
|
|
|
current[part] = {}
|
|
|
|
|
|
current = current[part]
|
|
|
|
|
|
|
|
|
|
|
|
# Convert value to appropriate type
|
|
|
|
|
|
if value.lower() in ('true', 'yes', 'on'):
|
|
|
|
|
|
value = True
|
|
|
|
|
|
elif value.lower() in ('false', 'no', 'off'):
|
|
|
|
|
|
value = False
|
|
|
|
|
|
elif value.isdigit():
|
|
|
|
|
|
value = int(value)
|
|
|
|
|
|
elif value.replace('.', '', 1).isdigit():
|
|
|
|
|
|
value = float(value)
|
|
|
|
|
|
|
|
|
|
|
|
current[parts[-1]] = value
|
2026-02-16 00:33:45 -08:00
|
|
|
|
|
|
|
|
|
|
# Write only user config back (not the full merged defaults)
|
|
|
|
|
|
ensure_hermes_home()
|
2026-03-05 17:04:33 -05:00
|
|
|
|
with open(config_path, 'w', encoding="utf-8") as f:
|
2026-02-16 00:33:45 -08:00
|
|
|
|
yaml.dump(user_config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
|
|
|
2026-02-26 20:02:46 -08:00
|
|
|
|
# Keep .env in sync for keys that terminal_tool reads directly from env vars.
|
|
|
|
|
|
# config.yaml is authoritative, but terminal_tool only reads TERMINAL_ENV etc.
|
|
|
|
|
|
_config_to_env_sync = {
|
|
|
|
|
|
"terminal.backend": "TERMINAL_ENV",
|
|
|
|
|
|
"terminal.docker_image": "TERMINAL_DOCKER_IMAGE",
|
|
|
|
|
|
"terminal.singularity_image": "TERMINAL_SINGULARITY_IMAGE",
|
|
|
|
|
|
"terminal.modal_image": "TERMINAL_MODAL_IMAGE",
|
2026-03-05 00:42:05 -08:00
|
|
|
|
"terminal.daytona_image": "TERMINAL_DAYTONA_IMAGE",
|
2026-02-26 20:02:46 -08:00
|
|
|
|
"terminal.cwd": "TERMINAL_CWD",
|
|
|
|
|
|
"terminal.timeout": "TERMINAL_TIMEOUT",
|
2026-03-08 01:33:46 -08:00
|
|
|
|
"terminal.sandbox_dir": "TERMINAL_SANDBOX_DIR",
|
2026-02-26 20:02:46 -08:00
|
|
|
|
}
|
|
|
|
|
|
if key in _config_to_env_sync:
|
|
|
|
|
|
save_env_value(_config_to_env_sync[key], str(value))
|
|
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
print(f"✓ Set {key} = {value} in {config_path}")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Command handler
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
def config_command(args):
|
|
|
|
|
|
"""Handle config subcommands."""
|
|
|
|
|
|
subcmd = getattr(args, 'config_command', None)
|
|
|
|
|
|
|
|
|
|
|
|
if subcmd is None or subcmd == "show":
|
|
|
|
|
|
show_config()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "edit":
|
|
|
|
|
|
edit_config()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "set":
|
|
|
|
|
|
key = getattr(args, 'key', None)
|
|
|
|
|
|
value = getattr(args, 'value', None)
|
|
|
|
|
|
if not key or not value:
|
|
|
|
|
|
print("Usage: hermes config set KEY VALUE")
|
|
|
|
|
|
print()
|
|
|
|
|
|
print("Examples:")
|
|
|
|
|
|
print(" hermes config set model anthropic/claude-sonnet-4")
|
|
|
|
|
|
print(" hermes config set terminal.backend docker")
|
|
|
|
|
|
print(" hermes config set OPENROUTER_API_KEY sk-or-...")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
set_config_value(key, value)
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "path":
|
|
|
|
|
|
print(get_config_path())
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "env-path":
|
|
|
|
|
|
print(get_env_path())
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
elif subcmd == "migrate":
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("🔄 Checking configuration for updates...", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Check what's missing
|
|
|
|
|
|
missing_env = get_missing_env_vars(required_only=False)
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
|
|
|
|
|
|
if not missing_env and not missing_config and current_ver >= latest_ver:
|
|
|
|
|
|
print(color("✓ Configuration is up to date!", Colors.GREEN))
|
|
|
|
|
|
print()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Show what needs to be updated
|
|
|
|
|
|
if current_ver < latest_ver:
|
|
|
|
|
|
print(f" Config version: {current_ver} → {latest_ver}")
|
|
|
|
|
|
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
print(f"\n {len(missing_config)} new config option(s) will be added with defaults")
|
|
|
|
|
|
|
|
|
|
|
|
required_missing = [v for v in missing_env if v.get("is_required")]
|
2026-02-15 21:53:59 -08:00
|
|
|
|
optional_missing = [
|
|
|
|
|
|
v for v in missing_env
|
|
|
|
|
|
if not v.get("is_required") and not v.get("advanced")
|
|
|
|
|
|
]
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
|
|
|
|
|
if required_missing:
|
|
|
|
|
|
print(f"\n ⚠️ {len(required_missing)} required API key(s) missing:")
|
|
|
|
|
|
for var in required_missing:
|
|
|
|
|
|
print(f" • {var['name']}")
|
|
|
|
|
|
|
|
|
|
|
|
if optional_missing:
|
|
|
|
|
|
print(f"\n ℹ️ {len(optional_missing)} optional API key(s) not configured:")
|
|
|
|
|
|
for var in optional_missing:
|
|
|
|
|
|
tools = var.get("tools", [])
|
|
|
|
|
|
tools_str = f" (enables: {', '.join(tools[:2])})" if tools else ""
|
|
|
|
|
|
print(f" • {var['name']}{tools_str}")
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
# Run migration
|
|
|
|
|
|
results = migrate_config(interactive=True, quiet=False)
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
if results["env_added"] or results["config_added"]:
|
|
|
|
|
|
print(color("✓ Configuration updated!", Colors.GREEN))
|
|
|
|
|
|
|
|
|
|
|
|
if results["warnings"]:
|
|
|
|
|
|
print()
|
|
|
|
|
|
for warning in results["warnings"]:
|
|
|
|
|
|
print(color(f" ⚠️ {warning}", Colors.YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
elif subcmd == "check":
|
|
|
|
|
|
# Non-interactive check for what's missing
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("📋 Configuration Status", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
|
|
current_ver, latest_ver = check_config_version()
|
|
|
|
|
|
if current_ver >= latest_ver:
|
|
|
|
|
|
print(f" Config version: {current_ver} ✓")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(color(f" Config version: {current_ver} → {latest_ver} (update available)", Colors.YELLOW))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(" Required:", Colors.BOLD))
|
|
|
|
|
|
for var_name in REQUIRED_ENV_VARS:
|
|
|
|
|
|
if get_env_value(var_name):
|
|
|
|
|
|
print(f" ✓ {var_name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(color(f" ✗ {var_name} (missing)", Colors.RED))
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(" Optional:", Colors.BOLD))
|
|
|
|
|
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
|
|
|
|
|
if get_env_value(var_name):
|
|
|
|
|
|
print(f" ✓ {var_name}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
tools = info.get("tools", [])
|
|
|
|
|
|
tools_str = f" → {', '.join(tools[:2])}" if tools else ""
|
|
|
|
|
|
print(color(f" ○ {var_name}{tools_str}", Colors.DIM))
|
|
|
|
|
|
|
|
|
|
|
|
missing_config = get_missing_config_fields()
|
|
|
|
|
|
if missing_config:
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color(f" {len(missing_config)} new config option(s) available", Colors.YELLOW))
|
2026-03-13 03:14:04 -07:00
|
|
|
|
print(" Run 'hermes config migrate' to add them")
|
2026-02-02 19:39:23 -08:00
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
2026-02-02 19:01:51 -08:00
|
|
|
|
else:
|
|
|
|
|
|
print(f"Unknown config command: {subcmd}")
|
2026-02-02 19:39:23 -08:00
|
|
|
|
print()
|
|
|
|
|
|
print("Available commands:")
|
|
|
|
|
|
print(" hermes config Show current configuration")
|
|
|
|
|
|
print(" hermes config edit Open config in editor")
|
|
|
|
|
|
print(" hermes config set K V Set a config value")
|
|
|
|
|
|
print(" hermes config check Check for missing/outdated config")
|
|
|
|
|
|
print(" hermes config migrate Update config with new options")
|
|
|
|
|
|
print(" hermes config path Show config file path")
|
|
|
|
|
|
print(" hermes config env-path Show .env file path")
|
2026-02-02 19:01:51 -08:00
|
|
|
|
sys.exit(1)
|