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
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
# ANSI colors
|
|
|
|
|
|
class Colors:
|
|
|
|
|
|
RESET = "\033[0m"
|
|
|
|
|
|
BOLD = "\033[1m"
|
|
|
|
|
|
DIM = "\033[2m"
|
|
|
|
|
|
RED = "\033[31m"
|
|
|
|
|
|
GREEN = "\033[32m"
|
|
|
|
|
|
YELLOW = "\033[33m"
|
|
|
|
|
|
BLUE = "\033[34m"
|
|
|
|
|
|
MAGENTA = "\033[35m"
|
|
|
|
|
|
CYAN = "\033[36m"
|
|
|
|
|
|
|
|
|
|
|
|
def color(text: str, *codes) -> str:
|
|
|
|
|
|
if not sys.stdout.isatty():
|
|
|
|
|
|
return text
|
|
|
|
|
|
return "".join(codes) + text + Colors.RESET
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_hermes_home():
|
|
|
|
|
|
"""Ensure ~/.hermes directory structure exists."""
|
|
|
|
|
|
home = get_hermes_home()
|
|
|
|
|
|
(home / "cron").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
(home / "sessions").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
(home / "logs").mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 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"],
|
|
|
|
|
|
"max_turns": 100,
|
|
|
|
|
|
|
|
|
|
|
|
"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-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"browser": {
|
|
|
|
|
|
"inactivity_timeout": 120,
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"compression": {
|
|
|
|
|
|
"enabled": True,
|
|
|
|
|
|
"threshold": 0.85,
|
2026-02-08 10:49:24 +00:00
|
|
|
|
"summary_model": "google/gemini-3-flash-preview",
|
2026-02-02 19:01:51 -08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"display": {
|
|
|
|
|
|
"compact": False,
|
|
|
|
|
|
"personality": "kawaii",
|
|
|
|
|
|
},
|
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-02 23:35:18 -08:00
|
|
|
|
# Permanently allowed dangerous command patterns (added via "always" approval)
|
|
|
|
|
|
"command_allowlist": [],
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# Config schema version - bump this when adding new required fields
|
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
|
|
|
|
"_config_version": 2,
|
2026-02-02 19:01:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 19:39:23 -08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Config Migration System
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
# Required environment variables with metadata for migration prompts
|
|
|
|
|
|
REQUIRED_ENV_VARS = {
|
|
|
|
|
|
"OPENROUTER_API_KEY": {
|
|
|
|
|
|
"description": "OpenRouter API key (required for vision, web scraping, and tools)",
|
|
|
|
|
|
"prompt": "OpenRouter API key",
|
|
|
|
|
|
"url": "https://openrouter.ai/keys",
|
|
|
|
|
|
"required": True,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Optional environment variables that enhance functionality
|
|
|
|
|
|
OPTIONAL_ENV_VARS = {
|
|
|
|
|
|
"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,
|
|
|
|
|
|
},
|
|
|
|
|
|
"BROWSERBASE_API_KEY": {
|
|
|
|
|
|
"description": "Browserbase API key for browser automation",
|
|
|
|
|
|
"prompt": "Browserbase API key",
|
|
|
|
|
|
"url": "https://browserbase.com/",
|
|
|
|
|
|
"tools": ["browser_navigate", "browser_click", "etc."],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"BROWSERBASE_PROJECT_ID": {
|
|
|
|
|
|
"description": "Browserbase project ID",
|
|
|
|
|
|
"prompt": "Browserbase project ID",
|
|
|
|
|
|
"url": "https://browserbase.com/",
|
|
|
|
|
|
"tools": ["browser_navigate", "browser_click", "etc."],
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
"FAL_KEY": {
|
|
|
|
|
|
"description": "FAL API key for image generation",
|
|
|
|
|
|
"prompt": "FAL API key",
|
|
|
|
|
|
"url": "https://fal.ai/",
|
|
|
|
|
|
"tools": ["image_generate"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
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,
|
|
|
|
|
|
},
|
|
|
|
|
|
"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-02 19:39:23 -08:00
|
|
|
|
"OPENAI_BASE_URL": {
|
2026-02-15 21:48:07 -08:00
|
|
|
|
"description": "Custom OpenAI-compatible API endpoint (for VLLM/SGLang/etc.)",
|
2026-02-15 21:53:59 -08:00
|
|
|
|
"prompt": "OpenAI-compatible base URL (only if running your own endpoint)",
|
2026-02-02 19:39:23 -08:00
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
2026-02-15 21:53:59 -08:00
|
|
|
|
"advanced": True, # Hide from standard migrate flow
|
2026-02-02 19:39:23 -08:00
|
|
|
|
},
|
2026-02-17 03:11:17 -08:00
|
|
|
|
"HERMES_OPENAI_API_KEY": {
|
|
|
|
|
|
"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,
|
|
|
|
|
|
},
|
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
|
|
|
|
"SLACK_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Slack bot integration",
|
|
|
|
|
|
"prompt": "Slack Bot Token (xoxb-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
|
|
|
|
|
"tools": ["slack"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"SLACK_APP_TOKEN": {
|
|
|
|
|
|
"description": "Slack Socket Mode connection",
|
|
|
|
|
|
"prompt": "Slack App Token (xapp-...)",
|
|
|
|
|
|
"url": "https://api.slack.com/apps",
|
|
|
|
|
|
"tools": ["slack"],
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
2026-02-03 10:46:23 -08:00
|
|
|
|
# Messaging platform tokens
|
|
|
|
|
|
"TELEGRAM_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Telegram bot token from @BotFather",
|
|
|
|
|
|
"prompt": "Telegram bot token",
|
|
|
|
|
|
"url": "https://t.me/BotFather",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"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,
|
|
|
|
|
|
},
|
|
|
|
|
|
"DISCORD_BOT_TOKEN": {
|
|
|
|
|
|
"description": "Discord bot token from Developer Portal",
|
|
|
|
|
|
"prompt": "Discord bot token",
|
|
|
|
|
|
"url": "https://discord.com/developers/applications",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
|
|
|
|
|
"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-12 10:05:08 -08:00
|
|
|
|
# Text-to-speech (premium providers)
|
|
|
|
|
|
"ELEVENLABS_API_KEY": {
|
|
|
|
|
|
"description": "ElevenLabs API key for premium text-to-speech voices",
|
|
|
|
|
|
"prompt": "ElevenLabs API key",
|
|
|
|
|
|
"url": "https://elevenlabs.io/",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
2026-02-03 10:46:23 -08:00
|
|
|
|
# Terminal configuration
|
|
|
|
|
|
"MESSAGING_CWD": {
|
|
|
|
|
|
"description": "Working directory for terminal commands via messaging (Telegram/Discord/etc). CLI always uses current directory.",
|
|
|
|
|
|
"prompt": "Messaging working directory (default: home)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
"SUDO_PASSWORD": {
|
|
|
|
|
|
"description": "Sudo password for terminal commands requiring root access",
|
|
|
|
|
|
"prompt": "Sudo password",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
2026-02-03 14:48:19 -08:00
|
|
|
|
# Agent configuration
|
|
|
|
|
|
"HERMES_MAX_ITERATIONS": {
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"description": "Maximum tool-calling iterations per conversation (default: 60)",
|
2026-02-03 14:48:19 -08:00
|
|
|
|
"prompt": "Max iterations",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
},
|
2026-02-03 14:54:43 -08:00
|
|
|
|
"HERMES_TOOL_PROGRESS": {
|
|
|
|
|
|
"description": "Send tool progress messages in messaging channels (true/false)",
|
|
|
|
|
|
"prompt": "Enable tool progress messages",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
},
|
|
|
|
|
|
"HERMES_TOOL_PROGRESS_MODE": {
|
|
|
|
|
|
"description": "Progress mode: 'all' (every tool) or 'new' (only when tool changes)",
|
|
|
|
|
|
"prompt": "Progress mode (all/new)",
|
|
|
|
|
|
"url": None,
|
|
|
|
|
|
"password": False,
|
|
|
|
|
|
},
|
Add Skills Hub — universal skill search, install, and management from online registries
Implements the Hermes Skills Hub with agentskills.io spec compliance,
multi-registry skill discovery, security scanning, and user-driven
management via CLI and /skills slash command.
Core features:
- Security scanner (tools/skills_guard.py): 120 threat patterns across
12 categories, trust-aware install policy (builtin/trusted/community),
structural checks, unicode injection detection, LLM audit pass
- Hub client (tools/skills_hub.py): GitHub, ClawHub, Claude Code
marketplace, and LobeHub source adapters with shared GitHubAuth
(PAT + gh CLI + GitHub App), lock file provenance tracking, quarantine
flow, and unified search across all sources
- CLI interface (hermes_cli/skills_hub.py): search, install, inspect,
list, audit, uninstall, publish (GitHub PR), snapshot export/import,
and tap management — powers both `hermes skills` and `/skills`
Spec conformance (Phase 0):
- Upgraded frontmatter parser to yaml.safe_load with fallback
- Migrated 39 SKILL.md files: tags/related_skills to metadata.hermes.*
- Added assets/ directory support and compatibility/metadata fields
- Excluded .hub/ from skill discovery in skills_tool.py
Updated 13 config/doc files including README, AGENTS.md, .env.example,
setup wizard, doctor, status, pyproject.toml, and docs.
2026-02-18 16:09:05 -08:00
|
|
|
|
"GITHUB_TOKEN": {
|
|
|
|
|
|
"description": "GitHub token for Skills Hub (higher API rate limits, skill publish)",
|
|
|
|
|
|
"prompt": "GitHub Token",
|
|
|
|
|
|
"url": "https://github.com/settings/tokens",
|
|
|
|
|
|
"password": True,
|
|
|
|
|
|
},
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_missing_config_fields() -> List[Dict[str, Any]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Check which config fields are missing or outdated.
|
|
|
|
|
|
|
|
|
|
|
|
Returns list of missing/outdated fields.
|
|
|
|
|
|
"""
|
|
|
|
|
|
config = load_config()
|
|
|
|
|
|
missing = []
|
|
|
|
|
|
|
|
|
|
|
|
# Check for new top-level keys in DEFAULT_CONFIG
|
|
|
|
|
|
for key, default_value in DEFAULT_CONFIG.items():
|
|
|
|
|
|
if key.startswith('_'):
|
|
|
|
|
|
continue # Skip internal keys
|
|
|
|
|
|
if key not in config:
|
|
|
|
|
|
missing.append({
|
|
|
|
|
|
"key": key,
|
|
|
|
|
|
"default": default_value,
|
|
|
|
|
|
"description": f"New config section: {key}",
|
|
|
|
|
|
})
|
|
|
|
|
|
elif isinstance(default_value, dict):
|
|
|
|
|
|
# Check nested keys
|
|
|
|
|
|
for subkey, subvalue in default_value.items():
|
|
|
|
|
|
if subkey not in config.get(key, {}):
|
|
|
|
|
|
missing.append({
|
|
|
|
|
|
"key": f"{key}.{subkey}",
|
|
|
|
|
|
"default": subvalue,
|
|
|
|
|
|
"description": f"New config option: {key}.{subkey}",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
if interactive and missing_optional:
|
2026-02-15 21:53:59 -08:00
|
|
|
|
print(" Would you like to configure any optional keys now?")
|
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
|
|
|
|
try:
|
|
|
|
|
|
answer = input(" Configure optional keys? [y/N]: ").strip().lower()
|
|
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
|
|
|
|
answer = "n"
|
|
|
|
|
|
|
|
|
|
|
|
if answer in ("y", "yes"):
|
|
|
|
|
|
print()
|
|
|
|
|
|
for var in missing_optional:
|
2026-02-15 21:53:59 -08:00
|
|
|
|
desc = var.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
|
|
|
|
if var.get("url"):
|
2026-02-15 21:53:59 -08:00
|
|
|
|
print(f" {desc}")
|
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(f" Get your key at: {var['url']}")
|
2026-02-15 21:53:59 -08:00
|
|
|
|
else:
|
|
|
|
|
|
print(f" {desc}")
|
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
|
|
|
|
|
|
|
|
|
|
if var.get("password"):
|
|
|
|
|
|
import getpass
|
|
|
|
|
|
value = getpass.getpass(f" {var['prompt']} (Enter to skip): ")
|
|
|
|
|
|
else:
|
|
|
|
|
|
value = input(f" {var['prompt']} (Enter to skip): ").strip()
|
|
|
|
|
|
|
|
|
|
|
|
if value:
|
|
|
|
|
|
save_env_value(var["name"], value)
|
|
|
|
|
|
results["env_added"].append(var["name"])
|
|
|
|
|
|
print(f" ✓ Saved {var['name']}")
|
|
|
|
|
|
print()
|
|
|
|
|
|
|
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"]
|
|
|
|
|
|
|
|
|
|
|
|
# Add with default value
|
|
|
|
|
|
if "." in key:
|
|
|
|
|
|
# Nested key
|
|
|
|
|
|
parent, child = key.split(".", 1)
|
|
|
|
|
|
if parent not in config:
|
|
|
|
|
|
config[parent] = {}
|
|
|
|
|
|
config[parent][child] = default
|
|
|
|
|
|
else:
|
|
|
|
|
|
config[key] = default
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
# Deep copy to avoid mutating DEFAULT_CONFIG
|
|
|
|
|
|
config = copy.deepcopy(DEFAULT_CONFIG)
|
2026-02-02 19:01:51 -08:00
|
|
|
|
|
|
|
|
|
|
if config_path.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(config_path) as f:
|
|
|
|
|
|
user_config = yaml.safe_load(f) or {}
|
|
|
|
|
|
|
2026-02-16 00:33:45 -08:00
|
|
|
|
# Deep merge user values over defaults
|
2026-02-02 19:01:51 -08:00
|
|
|
|
for key, value in user_config.items():
|
|
|
|
|
|
if isinstance(value, dict) and key in config and isinstance(config[key], dict):
|
|
|
|
|
|
config[key].update(value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
config[key] = value
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Warning: Failed to load config: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_config(config: Dict[str, Any]):
|
|
|
|
|
|
"""Save configuration to ~/.hermes/config.yaml."""
|
|
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
config_path = get_config_path()
|
|
|
|
|
|
|
|
|
|
|
|
with open(config_path, 'w') as f:
|
|
|
|
|
|
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_env() -> Dict[str, str]:
|
|
|
|
|
|
"""Load environment variables from ~/.hermes/.env."""
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
env_vars = {}
|
|
|
|
|
|
|
|
|
|
|
|
if env_path.exists():
|
|
|
|
|
|
with open(env_path) as f:
|
|
|
|
|
|
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."""
|
|
|
|
|
|
ensure_hermes_home()
|
|
|
|
|
|
env_path = get_env_path()
|
|
|
|
|
|
|
|
|
|
|
|
# Load existing
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
if env_path.exists():
|
|
|
|
|
|
with open(env_path) as f:
|
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
with open(env_path, 'w') as f:
|
|
|
|
|
|
f.writelines(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
env_vars = load_env()
|
|
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN))
|
|
|
|
|
|
print(color("│ 🦋 Hermes Configuration │", Colors.CYAN))
|
|
|
|
|
|
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"),
|
|
|
|
|
|
("ANTHROPIC_API_KEY", "Anthropic"),
|
2026-02-17 03:11:17 -08:00
|
|
|
|
("HERMES_OPENAI_API_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)}")
|
|
|
|
|
|
|
|
|
|
|
|
# Model settings
|
|
|
|
|
|
print()
|
|
|
|
|
|
print(color("◆ Model", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
|
print(f" Model: {config.get('model', 'not set')}")
|
|
|
|
|
|
print(f" Max turns: {config.get('max_turns', 100)}")
|
|
|
|
|
|
print(f" Toolsets: {', '.join(config.get('toolsets', ['all']))}")
|
|
|
|
|
|
|
|
|
|
|
|
# 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-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)'}")
|
|
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
|
print(f" Threshold: {compression.get('threshold', 0.85) * 100:.0f}%")
|
2026-02-08 10:49:24 +00:00
|
|
|
|
print(f" Model: {compression.get('summary_model', 'google/gemini-3-flash-preview')}")
|
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:
|
|
|
|
|
|
print(f"No editor found. Config file is at:")
|
|
|
|
|
|
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-02-17 03:11:17 -08:00
|
|
|
|
'OPENROUTER_API_KEY', 'ANTHROPIC_API_KEY', 'HERMES_OPENAI_API_KEY',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
'FIRECRAWL_API_KEY', 'BROWSERBASE_API_KEY', 'BROWSERBASE_PROJECT_ID',
|
|
|
|
|
|
'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',
|
Add Skills Hub — universal skill search, install, and management from online registries
Implements the Hermes Skills Hub with agentskills.io spec compliance,
multi-registry skill discovery, security scanning, and user-driven
management via CLI and /skills slash command.
Core features:
- Security scanner (tools/skills_guard.py): 120 threat patterns across
12 categories, trust-aware install policy (builtin/trusted/community),
structural checks, unicode injection detection, LLM audit pass
- Hub client (tools/skills_hub.py): GitHub, ClawHub, Claude Code
marketplace, and LobeHub source adapters with shared GitHubAuth
(PAT + gh CLI + GitHub App), lock file provenance tracking, quarantine
flow, and unified search across all sources
- CLI interface (hermes_cli/skills_hub.py): search, install, inspect,
list, audit, uninstall, publish (GitHub PR), snapshot export/import,
and tap management — powers both `hermes skills` and `/skills`
Spec conformance (Phase 0):
- Upgraded frontmatter parser to yaml.safe_load with fallback
- Migrated 39 SKILL.md files: tags/related_skills to metadata.hermes.*
- Added assets/ directory support and compatibility/metadata fields
- Excluded .hub/ from skill discovery in skills_tool.py
Updated 13 config/doc files including README, AGENTS.md, .env.example,
setup wizard, doctor, status, pyproject.toml, and docs.
2026-02-18 16:09:05 -08:00
|
|
|
|
'GITHUB_TOKEN',
|
2026-02-02 19:01:51 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if key.upper() in api_keys or key.upper().startswith('TERMINAL_SSH'):
|
|
|
|
|
|
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:
|
|
|
|
|
|
with open(config_path) as f:
|
|
|
|
|
|
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()
|
|
|
|
|
|
with open(config_path, 'w') as f:
|
|
|
|
|
|
yaml.dump(user_config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
print(f" Run 'hermes config migrate' to add them")
|
|
|
|
|
|
|
|
|
|
|
|
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)
|