2026-02-02 19:01:51 -08:00
"""
Interactive setup wizard for Hermes Agent .
Guides users through :
1. Installation directory confirmation
2. API key configuration
3. Model selection
4. Terminal backend selection
5. Messaging platform setup
6. Optional features
Config files are stored in ~ / . hermes / for easy access .
"""
2026-02-21 03:32:11 -08:00
import logging
2026-02-02 19:01:51 -08:00
import os
import sys
from pathlib import Path
from typing import Optional , Dict , Any
2026-02-21 03:32:11 -08:00
logger = logging . getLogger ( __name__ )
2026-02-02 19:01:51 -08:00
PROJECT_ROOT = Path ( __file__ ) . parent . parent . resolve ( )
# Import config helpers
from hermes_cli . config import (
get_hermes_home , get_config_path , get_env_path ,
load_config , save_config , save_env_value , get_env_value ,
ensure_hermes_home , DEFAULT_CONFIG
)
2026-02-20 23:23:32 -08:00
from hermes_cli . colors import Colors , color
2026-02-02 19:01:51 -08:00
def print_header ( title : str ) :
""" Print a section header. """
print ( )
print ( color ( f " ◆ { title } " , Colors . CYAN , Colors . BOLD ) )
def print_info ( text : str ) :
""" Print info text. """
print ( color ( f " { text } " , Colors . DIM ) )
def print_success ( text : str ) :
""" Print success message. """
print ( color ( f " ✓ { text } " , Colors . GREEN ) )
def print_warning ( text : str ) :
""" Print warning message. """
print ( color ( f " ⚠ { text } " , Colors . YELLOW ) )
def print_error ( text : str ) :
""" Print error message. """
print ( color ( f " ✗ { text } " , Colors . RED ) )
def prompt ( question : str , default : str = None , password : bool = False ) - > str :
""" Prompt for input with optional default. """
if default :
display = f " { question } [ { default } ]: "
else :
display = f " { question } : "
try :
if password :
import getpass
value = getpass . getpass ( color ( display , Colors . YELLOW ) )
else :
value = input ( color ( display , Colors . YELLOW ) )
return value . strip ( ) or default or " "
except ( KeyboardInterrupt , EOFError ) :
print ( )
sys . exit ( 1 )
def prompt_choice ( question : str , choices : list , default : int = 0 ) - > int :
2026-02-02 19:13:41 -08:00
""" Prompt for a choice from a list with arrow key navigation. """
2026-02-02 19:01:51 -08:00
print ( color ( question , Colors . YELLOW ) )
2026-02-02 19:13:41 -08:00
# Try to use interactive menu if available
try :
from simple_term_menu import TerminalMenu
# Add visual indicators
menu_choices = [ f " { choice } " for choice in choices ]
terminal_menu = TerminalMenu (
menu_choices ,
cursor_index = default ,
menu_cursor = " → " ,
menu_cursor_style = ( " fg_green " , " bold " ) ,
menu_highlight_style = ( " fg_green " , ) ,
cycle_cursor = True ,
clear_screen = False ,
)
idx = terminal_menu . show ( )
if idx is None : # User pressed Escape or Ctrl+C
2026-02-02 19:01:51 -08:00
print ( )
sys . exit ( 1 )
2026-02-02 19:13:41 -08:00
print ( ) # Add newline after selection
return idx
2026-02-25 14:10:54 -08:00
except ( ImportError , NotImplementedError ) :
# Fallback to number-based selection (simple_term_menu doesn't support Windows)
2026-02-02 19:13:41 -08:00
for i , choice in enumerate ( choices ) :
marker = " ● " if i == default else " ○ "
if i == default :
print ( color ( f " { marker } { choice } " , Colors . GREEN ) )
else :
print ( f " { marker } { choice } " )
while True :
try :
value = input ( color ( f " Select [1- { len ( choices ) } ] ( { default + 1 } ): " , Colors . DIM ) )
if not value :
return default
idx = int ( value ) - 1
if 0 < = idx < len ( choices ) :
return idx
print_error ( f " Please enter a number between 1 and { len ( choices ) } " )
except ValueError :
print_error ( " Please enter a number " )
except ( KeyboardInterrupt , EOFError ) :
print ( )
sys . exit ( 1 )
2026-02-02 19:01:51 -08:00
def prompt_yes_no ( question : str , default : bool = True ) - > bool :
""" Prompt for yes/no. """
default_str = " Y/n " if default else " y/N "
while True :
value = input ( color ( f " { question } [ { default_str } ]: " , Colors . YELLOW ) ) . strip ( ) . lower ( )
if not value :
return default
if value in ( ' y ' , ' yes ' ) :
return True
if value in ( ' n ' , ' no ' ) :
return False
print_error ( " Please enter ' y ' or ' n ' " )
2026-02-23 23:06:47 +00:00
def prompt_checklist ( title : str , items : list , pre_selected : list = None ) - > list :
"""
Display a multi - select checklist and return the indices of selected items .
Each item in ` items ` is a display string . ` pre_selected ` is a list of
indices that should be checked by default . A " Continue → " option is
appended at the end — the user toggles items with Space and confirms
with Enter on " Continue → " .
Falls back to a numbered toggle interface when simple_term_menu is
unavailable .
Returns :
List of selected indices ( not including the Continue option ) .
"""
if pre_selected is None :
pre_selected = [ ]
print ( color ( title , Colors . YELLOW ) )
2026-02-23 23:57:31 +00:00
print_info ( " SPACE to toggle, ENTER to confirm. " )
2026-02-23 23:06:47 +00:00
print ( )
try :
from simple_term_menu import TerminalMenu
2026-02-25 21:13:35 -08:00
import re
2026-02-23 23:06:47 +00:00
2026-02-25 21:13:35 -08:00
# Strip emoji characters from menu labels — simple_term_menu miscalculates
# visual width of emojis on macOS, causing duplicated/garbled lines.
_emoji_re = re . compile (
" [ \U0001f300 - \U0001f9ff \U00002600 - \U000027bf \U0000fe00 - \U0000fe0f "
" \U0001fa00 - \U0001fa6f \U0001fa70 - \U0001faff \u200d ]+ " , flags = re . UNICODE
)
menu_items = [ f " { _emoji_re . sub ( ' ' , item ) . strip ( ) } " for item in items ]
2026-02-23 23:06:47 +00:00
2026-02-23 23:31:07 +00:00
# Map pre-selected indices to the actual menu entry strings
preselected = [ menu_items [ i ] for i in pre_selected if i < len ( menu_items ) ]
2026-02-23 23:06:47 +00:00
terminal_menu = TerminalMenu (
menu_items ,
multi_select = True ,
2026-02-23 23:57:31 +00:00
show_multi_select_hint = False ,
2026-02-23 23:06:47 +00:00
multi_select_cursor = " [✓] " ,
multi_select_select_on_accept = False ,
multi_select_empty_ok = True ,
preselected_entries = preselected if preselected else None ,
menu_cursor = " → " ,
menu_cursor_style = ( " fg_green " , " bold " ) ,
menu_highlight_style = ( " fg_green " , ) ,
cycle_cursor = True ,
clear_screen = False ,
)
terminal_menu . show ( )
if terminal_menu . chosen_menu_entries is None :
return [ ]
2026-02-23 23:57:31 +00:00
selected = list ( terminal_menu . chosen_menu_indices or [ ] )
2026-02-23 23:06:47 +00:00
return selected
2026-02-25 14:10:54 -08:00
except ( ImportError , NotImplementedError ) :
# Fallback: numbered toggle interface (simple_term_menu doesn't support Windows)
2026-02-23 23:06:47 +00:00
selected = set ( pre_selected )
while True :
for i , item in enumerate ( items ) :
marker = color ( " [✓] " , Colors . GREEN ) if i in selected else " [ ] "
print ( f " { marker } { i + 1 } . { item } " )
print ( )
try :
2026-02-23 23:57:31 +00:00
value = input ( color ( " Toggle # (or Enter to confirm): " , Colors . DIM ) ) . strip ( )
2026-02-23 23:06:47 +00:00
if not value :
break
idx = int ( value ) - 1
if 0 < = idx < len ( items ) :
if idx in selected :
selected . discard ( idx )
else :
selected . add ( idx )
else :
print_error ( f " Enter a number between 1 and { len ( items ) + 1 } " )
except ValueError :
print_error ( " Enter a number " )
except ( KeyboardInterrupt , EOFError ) :
print ( )
return [ ]
# Clear and redraw (simple approach)
print ( )
return sorted ( selected )
2026-02-23 23:38:33 +00:00
def _prompt_api_key ( var : dict ) :
""" Display a nicely formatted API key input screen for a single env var. """
tools = var . get ( " tools " , [ ] )
tools_str = " , " . join ( tools [ : 3 ] )
if len ( tools ) > 3 :
tools_str + = f " , + { len ( tools ) - 3 } more "
print ( )
print ( color ( f " ─── { var . get ( ' description ' , var [ ' name ' ] ) } ─── " , Colors . CYAN ) )
print ( )
if tools_str :
print_info ( f " Enables: { tools_str } " )
if var . get ( " url " ) :
print_info ( f " Get your key at: { var [ ' url ' ] } " )
print ( )
if var . get ( " password " ) :
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " , password = True )
else :
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " )
if value :
save_env_value ( var [ " name " ] , value )
print_success ( f " ✓ Saved " )
else :
print_warning ( f " Skipped (configure later with ' hermes setup ' ) " )
2026-02-02 19:39:23 -08:00
def _print_setup_summary ( config : dict , hermes_home ) :
""" Print the setup completion summary. """
# Tool availability summary
print ( )
print_header ( " Tool Availability Summary " )
tool_status = [ ]
# OpenRouter (required for vision, moa)
if get_env_value ( ' OPENROUTER_API_KEY ' ) :
tool_status . append ( ( " Vision (image analysis) " , True , None ) )
tool_status . append ( ( " Mixture of Agents " , True , None ) )
else :
tool_status . append ( ( " Vision (image analysis) " , False , " OPENROUTER_API_KEY " ) )
tool_status . append ( ( " Mixture of Agents " , False , " OPENROUTER_API_KEY " ) )
# Firecrawl (web tools)
if get_env_value ( ' FIRECRAWL_API_KEY ' ) :
tool_status . append ( ( " Web Search & Extract " , True , None ) )
else :
tool_status . append ( ( " Web Search & Extract " , False , " FIRECRAWL_API_KEY " ) )
# Browserbase (browser tools)
if get_env_value ( ' BROWSERBASE_API_KEY ' ) :
tool_status . append ( ( " Browser Automation " , True , None ) )
else :
tool_status . append ( ( " Browser Automation " , False , " BROWSERBASE_API_KEY " ) )
# FAL (image generation)
if get_env_value ( ' FAL_KEY ' ) :
tool_status . append ( ( " Image Generation " , True , None ) )
else :
tool_status . append ( ( " Image Generation " , False , " FAL_KEY " ) )
2026-02-12 10:05:08 -08:00
# TTS (always available via Edge TTS; ElevenLabs/OpenAI are optional)
tool_status . append ( ( " Text-to-Speech (Edge TTS) " , True , None ) )
if get_env_value ( ' ELEVENLABS_API_KEY ' ) :
tool_status . append ( ( " Text-to-Speech (ElevenLabs) " , True , None ) )
2026-02-04 09:36:51 -08:00
# Tinker + WandB (RL training)
if get_env_value ( ' TINKER_API_KEY ' ) and get_env_value ( ' WANDB_API_KEY ' ) :
tool_status . append ( ( " RL Training (Tinker) " , True , None ) )
elif get_env_value ( ' TINKER_API_KEY ' ) :
tool_status . append ( ( " RL Training (Tinker) " , False , " WANDB_API_KEY " ) )
else :
tool_status . append ( ( " RL Training (Tinker) " , False , " TINKER_API_KEY " ) )
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
# Skills Hub
if get_env_value ( ' GITHUB_TOKEN ' ) :
tool_status . append ( ( " Skills Hub (GitHub) " , True , None ) )
else :
tool_status . append ( ( " Skills Hub (GitHub) " , False , " GITHUB_TOKEN " ) )
2026-02-02 19:39:23 -08:00
# Terminal (always available if system deps met)
tool_status . append ( ( " Terminal/Commands " , True , None ) )
2026-02-17 23:30:31 -08:00
# Task planning (always available, in-memory)
tool_status . append ( ( " Task Planning (todo) " , True , None ) )
2026-02-19 18:25:53 -08:00
# Skills (always available -- bundled skills + user-created skills)
tool_status . append ( ( " Skills (view, create, edit) " , True , None ) )
2026-02-02 19:39:23 -08:00
# Print status
available_count = sum ( 1 for _ , avail , _ in tool_status if avail )
total_count = len ( tool_status )
print_info ( f " { available_count } / { total_count } tool categories available: " )
print ( )
for name , available , missing_var in tool_status :
if available :
print ( f " { color ( ' ✓ ' , Colors . GREEN ) } { name } " )
else :
print ( f " { color ( ' ✗ ' , Colors . RED ) } { name } { color ( f ' (missing { missing_var } ) ' , Colors . DIM ) } " )
print ( )
disabled_tools = [ ( name , var ) for name , avail , var in tool_status if not avail ]
if disabled_tools :
print_warning ( " Some tools are disabled. Run ' hermes setup ' again to configure them, " )
print_warning ( " or edit ~/.hermes/.env directly to add the missing API keys. " )
print ( )
# Done banner
print ( )
print ( color ( " ┌─────────────────────────────────────────────────────────┐ " , Colors . GREEN ) )
print ( color ( " │ ✓ Setup Complete! │ " , Colors . GREEN ) )
print ( color ( " └─────────────────────────────────────────────────────────┘ " , Colors . GREEN ) )
print ( )
# Show file locations prominently
print ( color ( " 📁 All your files are in ~/.hermes/: " , Colors . CYAN , Colors . BOLD ) )
print ( )
print ( f " { color ( ' Settings: ' , Colors . YELLOW ) } { get_config_path ( ) } " )
print ( f " { color ( ' API Keys: ' , Colors . YELLOW ) } { get_env_path ( ) } " )
print ( f " { color ( ' Data: ' , Colors . YELLOW ) } { hermes_home } /cron/, sessions/, logs/ " )
print ( )
print ( color ( " ─ " * 60 , Colors . DIM ) )
print ( )
print ( color ( " 📝 To edit your configuration: " , Colors . CYAN , Colors . BOLD ) )
print ( )
print ( f " { color ( ' hermes config ' , Colors . GREEN ) } View current settings " )
print ( f " { color ( ' hermes config edit ' , Colors . GREEN ) } Open config in your editor " )
print ( f " { color ( ' hermes config set KEY VALUE ' , Colors . GREEN ) } " )
print ( f " Set a specific value " )
print ( )
print ( f " Or edit the files directly: " )
print ( f " { color ( f ' nano { get_config_path ( ) } ' , Colors . DIM ) } " )
print ( f " { color ( f ' nano { get_env_path ( ) } ' , Colors . DIM ) } " )
print ( )
print ( color ( " ─ " * 60 , Colors . DIM ) )
print ( )
print ( color ( " 🚀 Ready to go! " , Colors . CYAN , Colors . BOLD ) )
print ( )
print ( f " { color ( ' hermes ' , Colors . GREEN ) } Start chatting " )
print ( f " { color ( ' hermes gateway ' , Colors . GREEN ) } Start messaging gateway " )
print ( f " { color ( ' hermes doctor ' , Colors . GREEN ) } Check for issues " )
print ( )
2026-02-02 19:01:51 -08:00
def run_setup_wizard ( args ) :
""" Run the interactive setup wizard. """
ensure_hermes_home ( )
config = load_config ( )
hermes_home = get_hermes_home ( )
2026-02-23 23:06:47 +00:00
# Check if this is an existing installation with config (any provider or config file)
is_existing = (
get_env_value ( " OPENROUTER_API_KEY " ) is not None
or get_env_value ( " OPENAI_BASE_URL " ) is not None
or get_config_path ( ) . exists ( )
)
2026-02-02 19:39:23 -08:00
# Import migration helpers
from hermes_cli . config import (
get_missing_env_vars , get_missing_config_fields ,
check_config_version , migrate_config ,
REQUIRED_ENV_VARS , OPTIONAL_ENV_VARS
)
# Check what's missing
missing_required = [ v for v in get_missing_env_vars ( required_only = False ) if v . get ( " is_required " ) ]
missing_optional = [ v for v in get_missing_env_vars ( required_only = False ) if not v . get ( " is_required " ) ]
missing_config = get_missing_config_fields ( )
current_ver , latest_ver = check_config_version ( )
has_missing = missing_required or missing_optional or missing_config or current_ver < latest_ver
2026-02-02 19:01:51 -08:00
print ( )
print ( color ( " ┌─────────────────────────────────────────────────────────┐ " , Colors . MAGENTA ) )
2026-02-20 21:25:04 -08:00
print ( color ( " │ ⚕ Hermes Agent Setup Wizard │ " , Colors . MAGENTA ) )
2026-02-02 19:01:51 -08:00
print ( color ( " ├─────────────────────────────────────────────────────────┤ " , Colors . MAGENTA ) )
print ( color ( " │ Let ' s configure your Hermes Agent installation. │ " , Colors . MAGENTA ) )
print ( color ( " │ Press Ctrl+C at any time to exit. │ " , Colors . MAGENTA ) )
print ( color ( " └─────────────────────────────────────────────────────────┘ " , Colors . MAGENTA ) )
2026-02-02 19:39:23 -08:00
# If existing installation, show what's missing and offer quick mode
quick_mode = False
if is_existing and has_missing :
print ( )
print_header ( " Existing Installation Detected " )
print_success ( " You already have Hermes configured! " )
print ( )
if missing_required :
print_warning ( f " { len ( missing_required ) } required setting(s) missing: " )
for var in missing_required :
print ( f " • { var [ ' name ' ] } " )
if missing_optional :
print_info ( f " { len ( missing_optional ) } optional tool(s) not configured: " )
for var in missing_optional [ : 3 ] : # Show first 3
tools = var . get ( " tools " , [ ] )
tools_str = f " → { ' , ' . join ( tools [ : 2 ] ) } " if tools else " "
print ( f " • { var [ ' name ' ] } { tools_str } " )
if len ( missing_optional ) > 3 :
print ( f " • ...and { len ( missing_optional ) - 3 } more " )
if missing_config :
print_info ( f " { len ( missing_config ) } new config option(s) available " )
print ( )
setup_choices = [
" Quick setup - just configure missing items " ,
" Full setup - reconfigure everything " ,
" Skip - exit setup "
]
choice = prompt_choice ( " What would you like to do? " , setup_choices , 0 )
if choice == 0 :
quick_mode = True
elif choice == 2 :
print ( )
print_info ( " Exiting. Run ' hermes setup ' again when ready. " )
return
# choice == 1 continues with full setup
elif is_existing and not has_missing :
print ( )
print_header ( " Configuration Status " )
print_success ( " Your configuration is complete! " )
print ( )
if not prompt_yes_no ( " Would you like to reconfigure anyway? " , False ) :
print ( )
print_info ( " Exiting. Your configuration is already set up. " )
print_info ( f " Config: { get_config_path ( ) } " )
print_info ( f " Secrets: { get_env_path ( ) } " )
return
# Quick mode: only configure missing items
if quick_mode :
print ( )
print_header ( " Quick Setup - Missing Items Only " )
# Handle missing required env vars
if missing_required :
for var in missing_required :
print ( )
print ( color ( f " { var [ ' name ' ] } " , Colors . CYAN ) )
print_info ( f " { var . get ( ' description ' , ' ' ) } " )
if var . get ( " url " ) :
print_info ( f " Get key at: { var [ ' url ' ] } " )
if var . get ( " password " ) :
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " , password = True )
else :
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " )
if value :
save_env_value ( var [ " name " ] , value )
print_success ( f " Saved { var [ ' name ' ] } " )
else :
print_warning ( f " Skipped { var [ ' name ' ] } " )
2026-02-23 23:25:38 +00:00
# Split missing optional vars by category
missing_tools = [ v for v in missing_optional if v . get ( " category " ) == " tool " ]
missing_messaging = [ v for v in missing_optional if v . get ( " category " ) == " messaging " and not v . get ( " advanced " ) ]
# Settings are silently applied with defaults in quick mode
# ── Tool API keys (checklist) ──
if missing_tools :
2026-02-02 19:39:23 -08:00
print ( )
2026-02-23 23:25:38 +00:00
print_header ( " Tool API Keys " )
2026-02-23 23:06:47 +00:00
checklist_labels = [ ]
2026-02-23 23:25:38 +00:00
for var in missing_tools :
2026-02-02 19:39:23 -08:00
tools = var . get ( " tools " , [ ] )
2026-02-23 23:06:47 +00:00
tools_str = f " → { ' , ' . join ( tools [ : 2 ] ) } " if tools else " "
2026-02-23 23:25:38 +00:00
checklist_labels . append ( f " { var . get ( ' description ' , var [ ' name ' ] ) } { tools_str } " )
2026-02-23 23:06:47 +00:00
selected_indices = prompt_checklist (
2026-02-23 23:25:38 +00:00
" Which tools would you like to configure? " ,
2026-02-23 23:06:47 +00:00
checklist_labels ,
)
2026-02-23 23:25:38 +00:00
2026-02-23 23:06:47 +00:00
for idx in selected_indices :
2026-02-23 23:25:38 +00:00
var = missing_tools [ idx ]
2026-02-23 23:38:33 +00:00
_prompt_api_key ( var )
2026-02-23 23:25:38 +00:00
2026-02-23 23:32:32 +00:00
# ── Messaging platforms (checklist then prompt for selected) ──
2026-02-23 23:25:38 +00:00
if missing_messaging :
print ( )
print_header ( " Messaging Platforms " )
print_info ( " Connect Hermes to messaging apps to chat from anywhere. " )
print_info ( " You can configure these later with ' hermes setup ' . " )
2026-02-23 23:32:32 +00:00
# Group by platform (preserving order)
platform_order = [ ]
2026-02-23 23:25:38 +00:00
platforms = { }
for var in missing_messaging :
name = var [ " name " ]
if " TELEGRAM " in name :
2026-02-23 23:32:32 +00:00
plat = " Telegram "
2026-02-23 23:25:38 +00:00
elif " DISCORD " in name :
2026-02-23 23:32:32 +00:00
plat = " Discord "
2026-02-23 23:25:38 +00:00
elif " SLACK " in name :
2026-02-23 23:32:32 +00:00
plat = " Slack "
else :
continue
if plat not in platforms :
platform_order . append ( plat )
platforms . setdefault ( plat , [ ] ) . append ( var )
platform_labels = [
{ " Telegram " : " 📱 Telegram " , " Discord " : " 💬 Discord " , " Slack " : " 💼 Slack " } . get ( p , p )
for p in platform_order
]
2026-02-23 23:25:38 +00:00
2026-02-23 23:32:32 +00:00
selected_indices = prompt_checklist (
" Which platforms would you like to set up? " ,
platform_labels ,
)
for idx in selected_indices :
plat = platform_order [ idx ]
vars_list = platforms [ plat ]
2026-02-23 23:38:33 +00:00
emoji = { " Telegram " : " 📱 " , " Discord " : " 💬 " , " Slack " : " 💼 " } . get ( plat , " " )
print ( )
print ( color ( f " ─── { emoji } { plat } ─── " , Colors . CYAN ) )
2026-02-23 23:25:38 +00:00
print ( )
2026-02-23 23:32:32 +00:00
for var in vars_list :
2026-02-23 23:38:33 +00:00
print_info ( f " { var . get ( ' description ' , ' ' ) } " )
if var . get ( " url " ) :
print_info ( f " { var [ ' url ' ] } " )
2026-02-23 23:32:32 +00:00
if var . get ( " password " ) :
2026-02-23 23:38:33 +00:00
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " , password = True )
2026-02-23 23:32:32 +00:00
else :
2026-02-23 23:38:33 +00:00
value = prompt ( f " { var . get ( ' prompt ' , var [ ' name ' ] ) } " )
2026-02-23 23:32:32 +00:00
if value :
save_env_value ( var [ " name " ] , value )
2026-02-23 23:38:33 +00:00
print_success ( f " ✓ Saved " )
else :
print_warning ( f " Skipped " )
print ( )
2026-02-02 19:39:23 -08:00
# Handle missing config fields
if missing_config :
print ( )
print_info ( f " Adding { len ( missing_config ) } new config option(s) with defaults... " )
for field in missing_config :
print_success ( f " Added { field [ ' key ' ] } = { field [ ' default ' ] } " )
# Update config version
config [ " _config_version " ] = latest_ver
save_config ( config )
# Jump to summary
_print_setup_summary ( config , hermes_home )
return
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-02 19:39:23 -08:00
# Step 0: Show paths (full setup)
2026-02-02 19:01:51 -08:00
# =========================================================================
print_header ( " Configuration Location " )
print_info ( f " Config file: { get_config_path ( ) } " )
print_info ( f " Secrets file: { get_env_path ( ) } " )
print_info ( f " Data folder: { hermes_home } " )
print_info ( f " Install dir: { PROJECT_ROOT } " )
print ( )
print_info ( " You can edit these files directly or use ' hermes config edit ' " )
# =========================================================================
2026-02-20 17:24:00 -08:00
# Step 1: Inference Provider Selection
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-20 17:24:00 -08:00
print_header ( " Inference Provider " )
2026-02-02 19:23:20 -08:00
print_info ( " Choose how to connect to your main chat model. " )
2026-02-20 17:24:00 -08:00
print ( )
# Detect current provider state
from hermes_cli . auth import (
get_active_provider , get_provider_auth_state , PROVIDER_REGISTRY ,
format_auth_error , AuthError , fetch_nous_models ,
resolve_nous_runtime_credentials , _update_config_for_provider ,
2026-02-25 18:20:38 -08:00
_login_openai_codex , get_codex_auth_status , DEFAULT_CODEX_BASE_URL ,
2026-02-28 21:47:51 -08:00
detect_external_credentials ,
2026-02-20 17:24:00 -08:00
)
2026-02-02 19:13:41 -08:00
existing_custom = get_env_value ( " OPENAI_BASE_URL " )
2026-02-20 17:24:00 -08:00
existing_or = get_env_value ( " OPENROUTER_API_KEY " )
active_oauth = get_active_provider ( )
2026-02-28 21:47:51 -08:00
# Detect credentials from other CLI tools
detected_creds = detect_external_credentials ( )
if detected_creds :
print_info ( " Detected existing credentials: " )
for cred in detected_creds :
if cred [ " provider " ] == " openai-codex " :
print_success ( f " * { cred [ ' label ' ] } -- select \" OpenAI Codex \" to use it " )
else :
print_info ( f " * { cred [ ' label ' ] } " )
print ( )
2026-02-23 23:06:47 +00:00
# Detect if any provider is already configured
has_any_provider = bool ( active_oauth or existing_custom or existing_or )
2026-02-20 17:24:00 -08:00
# Build "keep current" label
if active_oauth and active_oauth in PROVIDER_REGISTRY :
keep_label = f " Keep current ( { PROVIDER_REGISTRY [ active_oauth ] . name } ) "
elif existing_custom :
keep_label = f " Keep current (Custom: { existing_custom } ) "
elif existing_or :
keep_label = " Keep current (OpenRouter) "
else :
2026-02-23 23:06:47 +00:00
keep_label = None # No provider configured — don't show "Keep current"
2026-02-20 17:24:00 -08:00
2026-02-02 19:23:20 -08:00
provider_choices = [
2026-02-20 17:24:00 -08:00
" Login with Nous Portal (Nous Research subscription) " ,
2026-02-25 18:25:15 -08:00
" Login with OpenAI Codex " ,
2026-02-20 17:24:00 -08:00
" OpenRouter API key (100+ models, pay-per-use) " ,
" Custom OpenAI-compatible endpoint (self-hosted / VLLM / etc.) " ,
2026-02-02 19:23:20 -08:00
]
2026-02-23 23:06:47 +00:00
if keep_label :
provider_choices . append ( keep_label )
# Default to "Keep current" if a provider exists, otherwise OpenRouter (most common)
2026-02-25 18:20:38 -08:00
default_provider = len ( provider_choices ) - 1 if has_any_provider else 2
2026-02-23 23:06:47 +00:00
if not has_any_provider :
print_warning ( " An inference provider is required for Hermes to work. " )
print ( )
provider_idx = prompt_choice ( " Select your inference provider: " , provider_choices , default_provider )
2026-02-20 17:24:00 -08:00
# Track which provider was selected for model step
2026-02-25 18:20:38 -08:00
selected_provider = None # "nous", "openai-codex", "openrouter", "custom", or None (keep)
2026-02-20 17:24:00 -08:00
nous_models = [ ] # populated if Nous login succeeds
if provider_idx == 0 : # Nous Portal
selected_provider = " nous "
print ( )
print_header ( " Nous Portal Login " )
print_info ( " This will open your browser to authenticate with Nous Portal. " )
print_info ( " You ' ll need a Nous Research account with an active subscription. " )
print ( )
try :
from hermes_cli . auth import _login_nous , ProviderConfig
import argparse
mock_args = argparse . Namespace (
portal_url = None , inference_url = None , client_id = None ,
scope = None , no_browser = False , timeout = 15.0 ,
ca_bundle = None , insecure = False ,
)
pconfig = PROVIDER_REGISTRY [ " nous " ]
_login_nous ( mock_args , pconfig )
# Fetch models for the selection step
try :
creds = resolve_nous_runtime_credentials (
min_key_ttl_seconds = 5 * 60 , timeout_seconds = 15.0 ,
)
nous_models = fetch_nous_models (
inference_base_url = creds . get ( " base_url " , " " ) ,
api_key = creds . get ( " api_key " , " " ) ,
)
2026-02-21 03:32:11 -08:00
except Exception as e :
logger . debug ( " Could not fetch Nous models after login: %s " , e )
2026-02-20 17:24:00 -08:00
except SystemExit :
print_warning ( " Nous Portal login was cancelled or failed. " )
2026-02-28 21:47:51 -08:00
print_info ( " You can try again later with: hermes model " )
2026-02-20 17:24:00 -08:00
selected_provider = None
except Exception as e :
print_error ( f " Login failed: { e } " )
2026-02-28 21:47:51 -08:00
print_info ( " You can try again later with: hermes model " )
2026-02-20 17:24:00 -08:00
selected_provider = None
2026-02-25 18:20:38 -08:00
elif provider_idx == 1 : # OpenAI Codex
selected_provider = " openai-codex "
print ( )
print_header ( " OpenAI Codex Login " )
print ( )
try :
import argparse
mock_args = argparse . Namespace ( )
_login_openai_codex ( mock_args , PROVIDER_REGISTRY [ " openai-codex " ] )
# Clear custom endpoint vars that would override provider routing.
if existing_custom :
save_env_value ( " OPENAI_BASE_URL " , " " )
save_env_value ( " OPENAI_API_KEY " , " " )
_update_config_for_provider ( " openai-codex " , DEFAULT_CODEX_BASE_URL )
except SystemExit :
print_warning ( " OpenAI Codex login was cancelled or failed. " )
2026-02-28 21:47:51 -08:00
print_info ( " You can try again later with: hermes model " )
2026-02-25 18:20:38 -08:00
selected_provider = None
except Exception as e :
print_error ( f " Login failed: { e } " )
2026-02-28 21:47:51 -08:00
print_info ( " You can try again later with: hermes model " )
2026-02-25 18:20:38 -08:00
selected_provider = None
elif provider_idx == 2 : # OpenRouter
2026-02-20 17:24:00 -08:00
selected_provider = " openrouter "
print ( )
print_header ( " OpenRouter API Key " )
print_info ( " OpenRouter provides access to 100+ models from multiple providers. " )
print_info ( " Get your API key at: https://openrouter.ai/keys " )
if existing_or :
print_info ( f " Current: { existing_or [ : 8 ] } ... (configured) " )
if prompt_yes_no ( " Update OpenRouter API key? " , False ) :
api_key = prompt ( " OpenRouter API key " , password = True )
if api_key :
save_env_value ( " OPENROUTER_API_KEY " , api_key )
print_success ( " OpenRouter API key updated " )
else :
api_key = prompt ( " OpenRouter API key " , password = True )
if api_key :
save_env_value ( " OPENROUTER_API_KEY " , api_key )
print_success ( " OpenRouter API key saved " )
else :
print_warning ( " Skipped - agent won ' t work without an API key " )
# Clear any custom endpoint if switching to OpenRouter
2026-02-02 19:23:20 -08:00
if existing_custom :
save_env_value ( " OPENAI_BASE_URL " , " " )
save_env_value ( " OPENAI_API_KEY " , " " )
2026-02-20 17:24:00 -08:00
2026-02-25 18:20:38 -08:00
elif provider_idx == 3 : # Custom endpoint
2026-02-20 17:24:00 -08:00
selected_provider = " custom "
print ( )
print_header ( " Custom OpenAI-Compatible Endpoint " )
2026-02-02 19:23:20 -08:00
print_info ( " Works with any API that follows OpenAI ' s chat completions spec " )
2026-02-20 17:24:00 -08:00
2026-02-02 19:23:20 -08:00
current_url = get_env_value ( " OPENAI_BASE_URL " ) or " "
current_key = get_env_value ( " OPENAI_API_KEY " )
current_model = config . get ( ' model ' , ' ' )
2026-02-20 17:24:00 -08:00
2026-02-02 19:23:20 -08:00
if current_url :
print_info ( f " Current URL: { current_url } " )
if current_key :
print_info ( f " Current key: { current_key [ : 8 ] } ... (configured) " )
2026-02-20 17:24:00 -08:00
2026-02-02 19:23:20 -08:00
base_url = prompt ( " API base URL (e.g., https://api.example.com/v1) " , current_url )
api_key = prompt ( " API key " , password = True )
model_name = prompt ( " Model name (e.g., gpt-4, claude-3-opus) " , current_model )
2026-02-20 17:24:00 -08:00
2026-02-02 19:23:20 -08:00
if base_url :
save_env_value ( " OPENAI_BASE_URL " , base_url )
if api_key :
save_env_value ( " OPENAI_API_KEY " , api_key )
if model_name :
config [ ' model ' ] = model_name
2026-02-20 17:24:00 -08:00
save_env_value ( " LLM_MODEL " , model_name )
2026-02-02 19:23:20 -08:00
print_success ( " Custom endpoint configured " )
2026-02-25 18:20:38 -08:00
# else: provider_idx == 4 (Keep current) — only shown when a provider already exists
2026-02-20 17:24:00 -08:00
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-20 17:24:00 -08:00
# Step 1b: OpenRouter API Key for tools (if not already set)
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-20 17:24:00 -08:00
# Tools (vision, web, MoA) use OpenRouter independently of the main provider.
# Prompt for OpenRouter key if not set and a non-OpenRouter provider was chosen.
2026-02-25 18:20:38 -08:00
if selected_provider in ( " nous " , " openai-codex " , " custom " ) and not get_env_value ( " OPENROUTER_API_KEY " ) :
2026-02-20 17:24:00 -08:00
print ( )
print_header ( " OpenRouter API Key (for tools) " )
print_info ( " Tools like vision analysis, web search, and MoA use OpenRouter " )
print_info ( " independently of your main inference provider. " )
print_info ( " Get your API key at: https://openrouter.ai/keys " )
api_key = prompt ( " OpenRouter API key (optional, press Enter to skip) " , password = True )
if api_key :
save_env_value ( " OPENROUTER_API_KEY " , api_key )
print_success ( " OpenRouter API key saved (for tools) " )
else :
print_info ( " Skipped - some tools (vision, web scraping) won ' t work without this " )
# =========================================================================
# Step 2: Model Selection (adapts based on provider)
# =========================================================================
if selected_provider != " custom " : # Custom already prompted for model name
print_header ( " Default Model " )
current_model = config . get ( ' model ' , ' anthropic/claude-opus-4.6 ' )
print_info ( f " Current: { current_model } " )
if selected_provider == " nous " and nous_models :
# Dynamic model list from Nous Portal
model_choices = [ f " { m } " for m in nous_models ]
model_choices . append ( " Custom model " )
model_choices . append ( f " Keep current ( { current_model } ) " )
# Post-login validation: warn if current model might not be available
if current_model and current_model not in nous_models :
print_warning ( f " Your current model ( { current_model } ) may not be available via Nous Portal. " )
print_info ( " Select a model from the list, or keep current to use it anyway. " )
print ( )
model_idx = prompt_choice ( " Select default model: " , model_choices , len ( model_choices ) - 1 )
if model_idx < len ( nous_models ) :
config [ ' model ' ] = nous_models [ model_idx ]
save_env_value ( " LLM_MODEL " , nous_models [ model_idx ] )
elif model_idx == len ( nous_models ) : # Custom
custom = prompt ( " Enter model name " )
if custom :
config [ ' model ' ] = custom
save_env_value ( " LLM_MODEL " , custom )
# else: keep current
2026-02-25 18:20:38 -08:00
elif selected_provider == " openai-codex " :
2026-02-25 19:27:54 -08:00
from hermes_cli . codex_models import get_codex_model_ids
2026-02-28 21:47:51 -08:00
# Try to get the access token for live model discovery
_codex_token = None
try :
from hermes_cli . auth import resolve_codex_runtime_credentials
_codex_creds = resolve_codex_runtime_credentials ( )
_codex_token = _codex_creds . get ( " api_key " )
except Exception :
pass
codex_models = get_codex_model_ids ( access_token = _codex_token )
2026-02-25 18:20:38 -08:00
model_choices = [ f " { m } " for m in codex_models ]
model_choices . append ( " Custom model " )
model_choices . append ( f " Keep current ( { current_model } ) " )
keep_idx = len ( model_choices ) - 1
model_idx = prompt_choice ( " Select default model: " , model_choices , keep_idx )
if model_idx < len ( codex_models ) :
config [ ' model ' ] = codex_models [ model_idx ]
save_env_value ( " LLM_MODEL " , codex_models [ model_idx ] )
elif model_idx == len ( codex_models ) :
custom = prompt ( " Enter model name " )
if custom :
config [ ' model ' ] = custom
save_env_value ( " LLM_MODEL " , custom )
_update_config_for_provider ( " openai-codex " , DEFAULT_CODEX_BASE_URL )
2026-02-20 17:24:00 -08:00
else :
2026-02-22 02:16:11 -08:00
# Static list for OpenRouter / fallback (from canonical list)
from hermes_cli . models import model_ids , menu_labels
ids = model_ids ( )
model_choices = menu_labels ( ) + [
2026-02-20 17:24:00 -08:00
" Custom model " ,
2026-02-22 02:16:11 -08:00
f " Keep current ( { current_model } ) " ,
2026-02-20 17:24:00 -08:00
]
2026-02-22 02:16:11 -08:00
keep_idx = len ( model_choices ) - 1
model_idx = prompt_choice ( " Select default model: " , model_choices , keep_idx )
if model_idx < len ( ids ) :
config [ ' model ' ] = ids [ model_idx ]
save_env_value ( " LLM_MODEL " , ids [ model_idx ] )
elif model_idx == len ( ids ) : # Custom
2026-02-20 17:24:00 -08:00
custom = prompt ( " Enter model name (e.g., anthropic/claude-opus-4.6) " )
if custom :
config [ ' model ' ] = custom
save_env_value ( " LLM_MODEL " , custom )
2026-02-22 02:16:11 -08:00
# else: Keep current
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-02 19:23:20 -08:00
# Step 4: Terminal Backend
2026-02-02 19:01:51 -08:00
# =========================================================================
print_header ( " Terminal Backend " )
print_info ( " The terminal tool allows the agent to run commands. " )
current_backend = config . get ( ' terminal ' , { } ) . get ( ' backend ' , ' local ' )
print_info ( f " Current: { current_backend } " )
2026-02-02 19:15:30 -08:00
# Detect platform for backend availability
import platform
is_linux = platform . system ( ) == " Linux "
is_macos = platform . system ( ) == " Darwin "
is_windows = platform . system ( ) == " Windows "
# Build choices based on platform
2026-02-02 19:01:51 -08:00
terminal_choices = [
" Local (run commands on this machine - no isolation) " ,
" Docker (isolated containers - recommended for security) " ,
2026-02-02 19:15:30 -08:00
]
# Singularity/Apptainer is Linux-only (HPC)
if is_linux :
terminal_choices . append ( " Singularity/Apptainer (HPC clusters, shared compute) " )
terminal_choices . extend ( [
2026-02-02 19:13:41 -08:00
" Modal (cloud execution, GPU access, serverless) " ,
2026-02-02 19:01:51 -08:00
" SSH (run commands on a remote server) " ,
2026-02-02 19:13:41 -08:00
f " Keep current ( { current_backend } ) "
2026-02-02 19:15:30 -08:00
] )
# Build index map based on available choices
if is_linux :
backend_to_idx = { ' local ' : 0 , ' docker ' : 1 , ' singularity ' : 2 , ' modal ' : 3 , ' ssh ' : 4 }
idx_to_backend = { 0 : ' local ' , 1 : ' docker ' , 2 : ' singularity ' , 3 : ' modal ' , 4 : ' ssh ' }
keep_current_idx = 5
else :
backend_to_idx = { ' local ' : 0 , ' docker ' : 1 , ' modal ' : 2 , ' ssh ' : 3 }
idx_to_backend = { 0 : ' local ' , 1 : ' docker ' , 2 : ' modal ' , 3 : ' ssh ' }
keep_current_idx = 4
if current_backend == ' singularity ' :
print_warning ( " Singularity is only available on Linux - please select a different backend " )
2026-02-02 19:01:51 -08:00
# Default based on current
2026-02-02 19:15:30 -08:00
default_terminal = backend_to_idx . get ( current_backend , 0 )
2026-02-02 19:01:51 -08:00
2026-02-02 19:15:30 -08:00
terminal_idx = prompt_choice ( " Select terminal backend: " , terminal_choices , keep_current_idx )
2026-02-02 19:01:51 -08:00
2026-02-02 19:15:30 -08:00
# Map index to backend name (handles platform differences)
selected_backend = idx_to_backend . get ( terminal_idx )
if selected_backend == ' local ' :
2026-02-02 19:01:51 -08:00
config . setdefault ( ' terminal ' , { } ) [ ' backend ' ] = ' local '
2026-02-02 19:13:41 -08:00
print_info ( " Local Execution Configuration: " )
print_info ( " Commands run directly on this machine (no isolation) " )
2026-02-02 19:01:51 -08:00
2026-02-02 19:15:30 -08:00
if is_windows :
print_info ( " Note: On Windows, commands run via cmd.exe or PowerShell " )
2026-02-03 10:46:23 -08:00
# Messaging working directory configuration
print_info ( " " )
print_info ( " Working Directory for Messaging (Telegram/Discord/etc): " )
print_info ( " The CLI always uses the directory you run ' hermes ' from " )
print_info ( " But messaging bots need a static starting directory " )
current_cwd = get_env_value ( ' MESSAGING_CWD ' ) or str ( Path . home ( ) )
print_info ( f " Current: { current_cwd } " )
cwd_input = prompt ( " Messaging working directory " , current_cwd )
# Expand ~ to full path
if cwd_input . startswith ( ' ~ ' ) :
cwd_expanded = str ( Path . home ( ) ) + cwd_input [ 1 : ]
else :
cwd_expanded = cwd_input
save_env_value ( " MESSAGING_CWD " , cwd_expanded )
2026-02-02 19:13:41 -08:00
if prompt_yes_no ( " Enable sudo support? (allows agent to run sudo commands) " , False ) :
print_warning ( " SECURITY WARNING: Sudo password will be stored in plaintext " )
sudo_pass = prompt ( " Sudo password (leave empty to skip) " , password = True )
2026-02-02 19:01:51 -08:00
if sudo_pass :
save_env_value ( " SUDO_PASSWORD " , sudo_pass )
2026-02-02 19:13:41 -08:00
print_success ( " Sudo password saved " )
print_success ( " Terminal set to local " )
2026-02-02 19:01:51 -08:00
2026-02-02 19:15:30 -08:00
elif selected_backend == ' docker ' :
2026-02-02 19:01:51 -08:00
config . setdefault ( ' terminal ' , { } ) [ ' backend ' ] = ' docker '
2026-02-02 19:13:41 -08:00
default_docker = config . get ( ' terminal ' , { } ) . get ( ' docker_image ' , ' nikolaik/python-nodejs:python3.11-nodejs20 ' )
print_info ( " Docker Configuration: " )
2026-02-02 19:15:30 -08:00
if is_macos :
print_info ( " Requires Docker Desktop for Mac " )
elif is_windows :
print_info ( " Requires Docker Desktop for Windows " )
2026-02-02 19:13:41 -08:00
docker_image = prompt ( " Docker image " , default_docker )
2026-02-02 19:01:51 -08:00
config [ ' terminal ' ] [ ' docker_image ' ] = docker_image
print_success ( " Terminal set to Docker " )
2026-02-02 19:15:30 -08:00
elif selected_backend == ' singularity ' :
2026-02-02 19:13:41 -08:00
config . setdefault ( ' terminal ' , { } ) [ ' backend ' ] = ' singularity '
default_singularity = config . get ( ' terminal ' , { } ) . get ( ' singularity_image ' , ' docker://nikolaik/python-nodejs:python3.11-nodejs20 ' )
print_info ( " Singularity/Apptainer Configuration: " )
print_info ( " Requires apptainer or singularity to be installed " )
singularity_image = prompt ( " Image (docker:// prefix for Docker Hub) " , default_singularity )
config [ ' terminal ' ] [ ' singularity_image ' ] = singularity_image
print_success ( " Terminal set to Singularity/Apptainer " )
2026-02-02 19:15:30 -08:00
elif selected_backend == ' modal ' :
2026-02-02 19:13:41 -08:00
config . setdefault ( ' terminal ' , { } ) [ ' backend ' ] = ' modal '
default_modal = config . get ( ' terminal ' , { } ) . get ( ' modal_image ' , ' nikolaik/python-nodejs:python3.11-nodejs20 ' )
print_info ( " Modal Cloud Configuration: " )
print_info ( " Get credentials at: https://modal.com/settings " )
2026-02-07 00:05:04 +00:00
# Check if swe-rex[modal] is installed, install if missing
try :
from swerex . deployment . modal import ModalDeployment
print_info ( " swe-rex[modal] package: installed ✓ " )
except ImportError :
print_info ( " Installing required package: swe-rex[modal]... " )
import subprocess
2026-02-07 23:54:53 +00:00
import shutil
# Prefer uv for speed, fall back to pip
uv_bin = shutil . which ( " uv " )
if uv_bin :
result = subprocess . run (
[ uv_bin , " pip " , " install " , " swe-rex[modal]>=1.4.0 " ] ,
capture_output = True , text = True
)
else :
result = subprocess . run (
[ sys . executable , " -m " , " pip " , " install " , " swe-rex[modal]>=1.4.0 " ] ,
capture_output = True , text = True
)
2026-02-07 00:05:04 +00:00
if result . returncode == 0 :
print_success ( " swe-rex[modal] installed (includes modal + boto3) " )
else :
print_warning ( " Failed to install swe-rex[modal] — install manually: " )
2026-02-07 23:54:53 +00:00
print_info ( ' uv pip install " swe-rex[modal]>=1.4.0 " ' )
2026-02-07 00:05:04 +00:00
2026-02-02 19:13:41 -08:00
# Always show current status and allow reconfiguration
current_token = get_env_value ( ' MODAL_TOKEN_ID ' )
if current_token :
print_info ( f " Token ID: { current_token [ : 8 ] } ... (configured) " )
modal_image = prompt ( " Container image " , default_modal )
config [ ' terminal ' ] [ ' modal_image ' ] = modal_image
token_id = prompt ( " Modal token ID " , current_token or " " )
token_secret = prompt ( " Modal token secret " , password = True )
if token_id :
save_env_value ( " MODAL_TOKEN_ID " , token_id )
if token_secret :
save_env_value ( " MODAL_TOKEN_SECRET " , token_secret )
print_success ( " Terminal set to Modal " )
2026-02-02 19:15:30 -08:00
elif selected_backend == ' ssh ' :
2026-02-02 19:01:51 -08:00
config . setdefault ( ' terminal ' , { } ) [ ' backend ' ] = ' ssh '
2026-02-02 19:13:41 -08:00
print_info ( " SSH Remote Execution Configuration: " )
print_info ( " Commands will run on a remote server over SSH " )
2026-02-02 19:01:51 -08:00
current_host = get_env_value ( ' TERMINAL_SSH_HOST ' ) or ' '
current_user = get_env_value ( ' TERMINAL_SSH_USER ' ) or os . getenv ( " USER " , " " )
2026-02-02 19:13:41 -08:00
current_port = get_env_value ( ' TERMINAL_SSH_PORT ' ) or ' 22 '
current_key = get_env_value ( ' TERMINAL_SSH_KEY ' ) or ' ~/.ssh/id_rsa '
if current_host :
print_info ( f " Current host: { current_user } @ { current_host } : { current_port } " )
2026-02-02 19:01:51 -08:00
2026-02-02 19:13:41 -08:00
ssh_host = prompt ( " SSH host " , current_host )
ssh_user = prompt ( " SSH user " , current_user )
ssh_port = prompt ( " SSH port " , current_port )
ssh_key = prompt ( " SSH key path (or leave empty for ssh-agent) " , current_key )
2026-02-02 19:01:51 -08:00
if ssh_host :
save_env_value ( " TERMINAL_SSH_HOST " , ssh_host )
if ssh_user :
save_env_value ( " TERMINAL_SSH_USER " , ssh_user )
2026-02-02 19:13:41 -08:00
if ssh_port and ssh_port != ' 22 ' :
save_env_value ( " TERMINAL_SSH_PORT " , ssh_port )
2026-02-02 19:01:51 -08:00
if ssh_key :
save_env_value ( " TERMINAL_SSH_KEY " , ssh_key )
print_success ( " Terminal set to SSH " )
2026-02-02 19:15:30 -08:00
# else: Keep current (selected_backend is None)
2026-02-02 19:01:51 -08:00
2026-02-26 20:02:46 -08:00
# Sync terminal backend to .env so terminal_tool picks it up directly.
# config.yaml is the source of truth, but terminal_tool reads TERMINAL_ENV.
if selected_backend :
save_env_value ( " TERMINAL_ENV " , selected_backend )
docker_image = config . get ( ' terminal ' , { } ) . get ( ' docker_image ' )
if docker_image :
save_env_value ( " TERMINAL_DOCKER_IMAGE " , docker_image )
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-03 14:48:19 -08:00
# Step 5: Agent Settings
# =========================================================================
print_header ( " Agent Settings " )
# Max iterations
current_max = get_env_value ( ' HERMES_MAX_ITERATIONS ' ) or ' 60 '
print_info ( " Maximum tool-calling iterations per conversation. " )
print_info ( " Higher = more complex tasks, but costs more tokens. " )
print_info ( " Recommended: 30-60 for most tasks, 100+ for open exploration. " )
max_iter_str = prompt ( " Max iterations " , current_max )
try :
max_iter = int ( max_iter_str )
if max_iter > 0 :
save_env_value ( " HERMES_MAX_ITERATIONS " , str ( max_iter ) )
config [ ' max_turns ' ] = max_iter
print_success ( f " Max iterations set to { max_iter } " )
except ValueError :
print_warning ( " Invalid number, keeping current value " )
2026-02-28 00:05:58 -08:00
# Tool progress notifications
2026-02-03 14:54:43 -08:00
print_info ( " " )
2026-02-28 00:05:58 -08:00
print_info ( " Tool Progress Display " )
print_info ( " Controls how much tool activity is shown (CLI and messaging). " )
print_info ( " off — Silent, just the final response " )
print_info ( " new — Show tool name only when it changes (less noise) " )
print_info ( " all — Show every tool call with a short preview " )
print_info ( " verbose — Full args, results, and debug logs " )
current_mode = config . get ( " display " , { } ) . get ( " tool_progress " , " all " )
mode = prompt ( " Tool progress mode " , current_mode )
if mode . lower ( ) in ( " off " , " new " , " all " , " verbose " ) :
if " display " not in config :
config [ " display " ] = { }
config [ " display " ] [ " tool_progress " ] = mode . lower ( )
save_config ( config )
print_success ( f " Tool progress set to: { mode . lower ( ) } " )
2026-02-03 14:54:43 -08:00
else :
2026-02-28 00:05:58 -08:00
print_warning ( f " Unknown mode ' { mode } ' , keeping ' { current_mode } ' " )
2026-02-03 14:54:43 -08:00
2026-02-03 14:48:19 -08:00
# =========================================================================
# Step 6: Context Compression
2026-02-02 19:01:51 -08:00
# =========================================================================
print_header ( " Context Compression " )
2026-02-23 23:31:07 +00:00
print_info ( " Automatically summarizes old messages when context gets too long. " )
print_info ( " Higher threshold = compress later (use more context). Lower = compress sooner. " )
2026-02-02 19:01:51 -08:00
2026-02-23 23:31:07 +00:00
config . setdefault ( ' compression ' , { } ) [ ' enabled ' ] = True
2026-02-02 19:01:51 -08:00
2026-02-23 23:31:07 +00:00
current_threshold = config . get ( ' compression ' , { } ) . get ( ' threshold ' , 0.85 )
threshold_str = prompt ( " Compression threshold (0.5-0.95) " , str ( current_threshold ) )
try :
threshold = float ( threshold_str )
if 0.5 < = threshold < = 0.95 :
config [ ' compression ' ] [ ' threshold ' ] = threshold
except ValueError :
pass
print_success ( f " Context compression threshold set to { config [ ' compression ' ] . get ( ' threshold ' , 0.85 ) } " )
2026-02-02 19:01:51 -08:00
2026-02-26 21:20:50 -08:00
# =========================================================================
# Step 6b: Session Reset Policy (Messaging)
# =========================================================================
print_header ( " Session Reset Policy " )
print_info ( " Messaging sessions (Telegram, Discord, etc.) accumulate context over time. " )
print_info ( " Each message adds to the conversation history, which means growing API costs. " )
print_info ( " " )
print_info ( " To manage this, sessions can automatically reset after a period of inactivity " )
print_info ( " or at a fixed time each day. When a reset happens, the agent saves important " )
print_info ( " things to its persistent memory first — but the conversation context is cleared. " )
print_info ( " " )
print_info ( " You can also manually reset anytime by typing /reset in chat. " )
print_info ( " " )
reset_choices = [
" Inactivity + daily reset (recommended — reset whichever comes first) " ,
" Inactivity only (reset after N minutes of no messages) " ,
" Daily only (reset at a fixed hour each day) " ,
" Never auto-reset (context lives until /reset or context compression) " ,
" Keep current settings " ,
]
current_policy = config . get ( ' session_reset ' , { } )
current_mode = current_policy . get ( ' mode ' , ' both ' )
current_idle = current_policy . get ( ' idle_minutes ' , 1440 )
current_hour = current_policy . get ( ' at_hour ' , 4 )
default_reset = { " both " : 0 , " idle " : 1 , " daily " : 2 , " none " : 3 } . get ( current_mode , 0 )
reset_idx = prompt_choice ( " Session reset mode: " , reset_choices , default_reset )
config . setdefault ( ' session_reset ' , { } )
if reset_idx == 0 : # Both
config [ ' session_reset ' ] [ ' mode ' ] = ' both '
idle_str = prompt ( " Inactivity timeout (minutes) " , str ( current_idle ) )
try :
idle_val = int ( idle_str )
if idle_val > 0 :
config [ ' session_reset ' ] [ ' idle_minutes ' ] = idle_val
except ValueError :
pass
hour_str = prompt ( " Daily reset hour (0-23, local time) " , str ( current_hour ) )
try :
hour_val = int ( hour_str )
if 0 < = hour_val < = 23 :
config [ ' session_reset ' ] [ ' at_hour ' ] = hour_val
except ValueError :
pass
print_success ( f " Sessions reset after { config [ ' session_reset ' ] . get ( ' idle_minutes ' , 1440 ) } min idle or daily at { config [ ' session_reset ' ] . get ( ' at_hour ' , 4 ) } :00 " )
elif reset_idx == 1 : # Idle only
config [ ' session_reset ' ] [ ' mode ' ] = ' idle '
idle_str = prompt ( " Inactivity timeout (minutes) " , str ( current_idle ) )
try :
idle_val = int ( idle_str )
if idle_val > 0 :
config [ ' session_reset ' ] [ ' idle_minutes ' ] = idle_val
except ValueError :
pass
print_success ( f " Sessions reset after { config [ ' session_reset ' ] . get ( ' idle_minutes ' , 1440 ) } min of inactivity " )
elif reset_idx == 2 : # Daily only
config [ ' session_reset ' ] [ ' mode ' ] = ' daily '
hour_str = prompt ( " Daily reset hour (0-23, local time) " , str ( current_hour ) )
try :
hour_val = int ( hour_str )
if 0 < = hour_val < = 23 :
config [ ' session_reset ' ] [ ' at_hour ' ] = hour_val
except ValueError :
pass
print_success ( f " Sessions reset daily at { config [ ' session_reset ' ] . get ( ' at_hour ' , 4 ) } :00 " )
elif reset_idx == 3 : # None
config [ ' session_reset ' ] [ ' mode ' ] = ' none '
print_info ( " Sessions will never auto-reset. Context is managed only by compression. " )
print_warning ( " Long conversations will grow in cost. Use /reset manually when needed. " )
# else: keep current (idx == 4)
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-03 14:48:19 -08:00
# Step 7: Messaging Platforms (Optional)
2026-02-02 19:01:51 -08:00
# =========================================================================
print_header ( " Messaging Platforms (Optional) " )
print_info ( " Connect to messaging platforms to chat with Hermes from anywhere. " )
# Telegram
existing_telegram = get_env_value ( ' TELEGRAM_BOT_TOKEN ' )
if existing_telegram :
print_info ( " Telegram: already configured " )
if prompt_yes_no ( " Reconfigure Telegram? " , False ) :
existing_telegram = None
if not existing_telegram and prompt_yes_no ( " Set up Telegram bot? " , False ) :
print_info ( " Create a bot via @BotFather on Telegram " )
token = prompt ( " Telegram bot token " , password = True )
if token :
save_env_value ( " TELEGRAM_BOT_TOKEN " , token )
print_success ( " Telegram token saved " )
2026-02-03 10:46:23 -08:00
# Allowed users (security)
print ( )
print_info ( " 🔒 Security: Restrict who can use your bot " )
print_info ( " To find your Telegram user ID: " )
print_info ( " 1. Message @userinfobot on Telegram " )
print_info ( " 2. It will reply with your numeric ID (e.g., 123456789) " )
print ( )
allowed_users = prompt ( " Allowed user IDs (comma-separated, leave empty for open access) " )
if allowed_users :
save_env_value ( " TELEGRAM_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " Telegram allowlist configured - only listed users can use the bot " )
else :
print_info ( " ⚠️ No allowlist set - anyone who finds your bot can use it! " )
2026-02-22 20:44:15 -08:00
# Home channel setup with better guidance
print ( )
print_info ( " 📬 Home Channel: where Hermes delivers cron job results, " )
print_info ( " cross-platform messages, and notifications. " )
print_info ( " For Telegram DMs, this is your user ID (same as above). " )
first_user_id = allowed_users . split ( " , " ) [ 0 ] . strip ( ) if allowed_users else " "
if first_user_id :
if prompt_yes_no ( f " Use your user ID ( { first_user_id } ) as the home channel? " , True ) :
save_env_value ( " TELEGRAM_HOME_CHANNEL " , first_user_id )
print_success ( f " Telegram home channel set to { first_user_id } " )
else :
home_channel = prompt ( " Home channel ID (or leave empty to set later with /set-home in Telegram) " )
if home_channel :
save_env_value ( " TELEGRAM_HOME_CHANNEL " , home_channel )
else :
print_info ( " You can also set this later by typing /set-home in your Telegram chat. " )
home_channel = prompt ( " Home channel ID (leave empty to set later) " )
if home_channel :
save_env_value ( " TELEGRAM_HOME_CHANNEL " , home_channel )
2026-02-02 19:01:51 -08:00
2026-02-03 10:46:23 -08:00
# Check/update existing Telegram allowlist
elif existing_telegram :
existing_allowlist = get_env_value ( ' TELEGRAM_ALLOWED_USERS ' )
if not existing_allowlist :
print_info ( " ⚠️ Telegram has no user allowlist - anyone can use your bot! " )
if prompt_yes_no ( " Add allowed users now? " , True ) :
print_info ( " To find your Telegram user ID: message @userinfobot " )
allowed_users = prompt ( " Allowed user IDs (comma-separated) " )
if allowed_users :
save_env_value ( " TELEGRAM_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " Telegram allowlist configured " )
2026-02-02 19:01:51 -08:00
# Discord
existing_discord = get_env_value ( ' DISCORD_BOT_TOKEN ' )
if existing_discord :
print_info ( " Discord: already configured " )
if prompt_yes_no ( " Reconfigure Discord? " , False ) :
existing_discord = None
if not existing_discord and prompt_yes_no ( " Set up Discord bot? " , False ) :
print_info ( " Create a bot at https://discord.com/developers/applications " )
token = prompt ( " Discord bot token " , password = True )
if token :
save_env_value ( " DISCORD_BOT_TOKEN " , token )
print_success ( " Discord token saved " )
2026-02-03 10:46:23 -08:00
# Allowed users (security)
print ( )
print_info ( " 🔒 Security: Restrict who can use your bot " )
print_info ( " To find your Discord user ID: " )
print_info ( " 1. Enable Developer Mode in Discord settings " )
print_info ( " 2. Right-click your name → Copy ID " )
print ( )
2026-02-22 20:44:15 -08:00
print_info ( " You can also use Discord usernames (resolved on gateway start). " )
print ( )
allowed_users = prompt ( " Allowed user IDs or usernames (comma-separated, leave empty for open access) " )
2026-02-03 10:46:23 -08:00
if allowed_users :
save_env_value ( " DISCORD_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " Discord allowlist configured " )
else :
print_info ( " ⚠️ No allowlist set - anyone in servers with your bot can use it! " )
2026-02-22 20:44:15 -08:00
# Home channel setup with better guidance
print ( )
print_info ( " 📬 Home Channel: where Hermes delivers cron job results, " )
print_info ( " cross-platform messages, and notifications. " )
print_info ( " To get a channel ID: right-click a channel → Copy Channel ID " )
print_info ( " (requires Developer Mode in Discord settings) " )
print_info ( " You can also set this later by typing /set-home in a Discord channel. " )
home_channel = prompt ( " Home channel ID (leave empty to set later with /set-home) " )
2026-02-02 19:01:51 -08:00
if home_channel :
save_env_value ( " DISCORD_HOME_CHANNEL " , home_channel )
2026-02-03 10:46:23 -08:00
# Check/update existing Discord allowlist
elif existing_discord :
existing_allowlist = get_env_value ( ' DISCORD_ALLOWED_USERS ' )
if not existing_allowlist :
print_info ( " ⚠️ Discord has no user allowlist - anyone can use your bot! " )
if prompt_yes_no ( " Add allowed users now? " , True ) :
print_info ( " To find Discord ID: Enable Developer Mode, right-click name → Copy ID " )
allowed_users = prompt ( " Allowed user IDs (comma-separated) " )
if allowed_users :
save_env_value ( " DISCORD_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " Discord allowlist configured " )
2026-02-19 12:33:09 -08:00
# Slack
existing_slack = get_env_value ( ' SLACK_BOT_TOKEN ' )
if existing_slack :
print_info ( " Slack: already configured " )
if prompt_yes_no ( " Reconfigure Slack? " , False ) :
existing_slack = None
if not existing_slack and prompt_yes_no ( " Set up Slack bot? " , False ) :
print_info ( " Steps to create a Slack app: " )
print_info ( " 1. Go to https://api.slack.com/apps → Create New App " )
print_info ( " 2. Enable Socket Mode: App Settings → Socket Mode → Enable " )
print_info ( " 3. Bot Token: OAuth & Permissions → Install to Workspace " )
print_info ( " 4. App Token: Basic Information → App-Level Tokens → Generate " )
print ( )
bot_token = prompt ( " Slack Bot Token (xoxb-...) " , password = True )
if bot_token :
save_env_value ( " SLACK_BOT_TOKEN " , bot_token )
app_token = prompt ( " Slack App Token (xapp-...) " , password = True )
if app_token :
save_env_value ( " SLACK_APP_TOKEN " , app_token )
print_success ( " Slack tokens saved " )
print ( )
print_info ( " 🔒 Security: Restrict who can use your bot " )
print_info ( " Find Slack user IDs in your profile or via the Slack API " )
print ( )
allowed_users = prompt ( " Allowed user IDs (comma-separated, leave empty for open access) " )
if allowed_users :
save_env_value ( " SLACK_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " Slack allowlist configured " )
else :
print_info ( " ⚠️ No allowlist set - anyone in your workspace can use the bot! " )
# WhatsApp
existing_whatsapp = get_env_value ( ' WHATSAPP_ENABLED ' )
if not existing_whatsapp and prompt_yes_no ( " Set up WhatsApp? " , False ) :
2026-02-25 21:04:36 -08:00
print_info ( " WhatsApp connects via a built-in bridge (Baileys). " )
print_info ( " Requires Node.js (already installed if you have browser tools). " )
print_info ( " On first gateway start, you ' ll scan a QR code with your phone. " )
2026-02-19 12:33:09 -08:00
print ( )
2026-02-25 21:04:36 -08:00
if prompt_yes_no ( " Enable WhatsApp? " , True ) :
2026-02-19 12:33:09 -08:00
save_env_value ( " WHATSAPP_ENABLED " , " true " )
print_success ( " WhatsApp enabled " )
2026-02-25 21:04:36 -08:00
allowed_users = prompt ( " Your phone number (e.g. 15551234567, comma-separated for multiple) " )
if allowed_users :
save_env_value ( " WHATSAPP_ALLOWED_USERS " , allowed_users . replace ( " " , " " ) )
print_success ( " WhatsApp allowlist configured " )
else :
print_info ( " ⚠️ No allowlist set — anyone who messages your WhatsApp will get a response! " )
print_info ( " Start the gateway with ' hermes gateway ' and scan the QR code. " )
2026-02-19 12:33:09 -08:00
# Gateway reminder
any_messaging = (
get_env_value ( ' TELEGRAM_BOT_TOKEN ' )
or get_env_value ( ' DISCORD_BOT_TOKEN ' )
or get_env_value ( ' SLACK_BOT_TOKEN ' )
or get_env_value ( ' WHATSAPP_ENABLED ' )
)
if any_messaging :
print ( )
print_info ( " ━ " * 50 )
print_success ( " Messaging platforms configured! " )
print_info ( " Start the gateway after setup to bring your bots online: " )
print_info ( " hermes gateway # Run in foreground " )
print_info ( " hermes gateway install # Install as background service (Linux) " )
2026-02-22 20:44:15 -08:00
# Check if any home channels are missing
missing_home = [ ]
if get_env_value ( ' TELEGRAM_BOT_TOKEN ' ) and not get_env_value ( ' TELEGRAM_HOME_CHANNEL ' ) :
missing_home . append ( " Telegram " )
if get_env_value ( ' DISCORD_BOT_TOKEN ' ) and not get_env_value ( ' DISCORD_HOME_CHANNEL ' ) :
missing_home . append ( " Discord " )
if get_env_value ( ' SLACK_BOT_TOKEN ' ) and not get_env_value ( ' SLACK_HOME_CHANNEL ' ) :
missing_home . append ( " Slack " )
if missing_home :
print ( )
print_info ( f " ⚠️ No home channel set for: { ' , ' . join ( missing_home ) } " )
print_info ( " Without a home channel, cron jobs and cross-platform " )
print_info ( " messages can ' t be delivered to those platforms. " )
print_info ( " Set one later with /set-home in your chat, or: " )
for plat in missing_home :
print_info ( f " hermes config set { plat . upper ( ) } _HOME_CHANNEL <channel_id> " )
2026-02-19 12:33:09 -08:00
print_info ( " ━ " * 50 )
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-23 23:06:47 +00:00
# Step 8: Additional Tools (Checkbox Selection)
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-23 23:06:47 +00:00
print_header ( " Additional Tools " )
print_info ( " Select which tools you ' d like to configure. " )
print_info ( " You can always add more later with ' hermes setup ' . " )
2026-02-02 19:28:27 -08:00
print ( )
2026-02-02 19:01:51 -08:00
2026-02-23 23:06:47 +00:00
# Define tool categories for the checklist.
# Each entry: (display_label, setup_function_key, check_keys)
# check_keys = env vars that indicate this tool is already configured
TOOL_CATEGORIES = [
{
" label " : " 🔍 Web Search & Scraping (Firecrawl) " ,
" key " : " firecrawl " ,
" check " : [ " FIRECRAWL_API_KEY " ] ,
} ,
{
" label " : " 🌐 Browser Automation (Browserbase) " ,
" key " : " browserbase " ,
" check " : [ " BROWSERBASE_API_KEY " ] ,
} ,
{
" label " : " 🎨 Image Generation (FAL / FLUX) " ,
" key " : " fal " ,
" check " : [ " FAL_KEY " ] ,
} ,
{
" label " : " 🎤 Voice Transcription & TTS (OpenAI Whisper + TTS) " ,
" key " : " openai_voice " ,
2026-02-23 23:21:33 +00:00
" check " : [ " VOICE_TOOLS_OPENAI_KEY " ] ,
2026-02-23 23:06:47 +00:00
} ,
{
" label " : " 🗣️ Premium Text-to-Speech (ElevenLabs) " ,
" key " : " elevenlabs " ,
" check " : [ " ELEVENLABS_API_KEY " ] ,
} ,
{
" label " : " 🧪 RL Training (Tinker + WandB) " ,
" key " : " rl_training " ,
" check " : [ " TINKER_API_KEY " , " WANDB_API_KEY " ] ,
} ,
{
" label " : " 🔧 Skills Hub (GitHub token for higher rate limits) " ,
" key " : " github " ,
" check " : [ " GITHUB_TOKEN " ] ,
} ,
]
# Pre-select tools that are already configured
pre_selected = [ ]
for i , cat in enumerate ( TOOL_CATEGORIES ) :
if all ( get_env_value ( k ) for k in cat [ " check " ] ) :
pre_selected . append ( i )
checklist_labels = [ cat [ " label " ] for cat in TOOL_CATEGORIES ]
selected_indices = prompt_checklist (
" Which tools would you like to enable? " ,
checklist_labels ,
pre_selected = pre_selected ,
)
selected_keys = { TOOL_CATEGORIES [ i ] [ " key " ] for i in selected_indices }
# Now prompt for API keys only for the tools the user selected
if " firecrawl " in selected_keys :
print ( )
print ( color ( " ─── Web Search & Scraping (Firecrawl) ─── " , Colors . CYAN ) )
print_info ( " Get your API key at: https://firecrawl.dev/ " )
existing = get_env_value ( ' FIRECRAWL_API_KEY ' )
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update API key? " , False ) :
api_key = prompt ( " Firecrawl API key " , password = True )
if api_key :
save_env_value ( " FIRECRAWL_API_KEY " , api_key )
print_success ( " Updated " )
else :
api_key = prompt ( " Firecrawl API key " , password = True )
2026-02-02 19:28:27 -08:00
if api_key :
save_env_value ( " FIRECRAWL_API_KEY " , api_key )
print_success ( " Configured ✓ " )
2026-02-23 23:06:47 +00:00
if " browserbase " in selected_keys :
print ( )
print ( color ( " ─── Browser Automation (Browserbase) ─── " , Colors . CYAN ) )
print_info ( " Get credentials at: https://browserbase.com/ " )
existing = get_env_value ( ' BROWSERBASE_API_KEY ' )
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update credentials? " , False ) :
api_key = prompt ( " API key " , password = True )
project_id = prompt ( " Project ID " )
if api_key :
save_env_value ( " BROWSERBASE_API_KEY " , api_key )
if project_id :
save_env_value ( " BROWSERBASE_PROJECT_ID " , project_id )
print_success ( " Updated " )
else :
api_key = prompt ( " Browserbase API key " , password = True )
project_id = prompt ( " Browserbase Project ID " )
2026-02-02 19:28:27 -08:00
if api_key :
save_env_value ( " BROWSERBASE_API_KEY " , api_key )
if project_id :
save_env_value ( " BROWSERBASE_PROJECT_ID " , project_id )
2026-02-07 00:05:04 +00:00
2026-02-23 23:06:47 +00:00
# Auto-install Node.js deps if possible
2026-02-07 00:05:04 +00:00
import shutil
node_modules = PROJECT_ROOT / " node_modules " / " agent-browser "
if not node_modules . exists ( ) and shutil . which ( " npm " ) :
print_info ( " Installing Node.js dependencies for browser tools... " )
import subprocess
result = subprocess . run (
[ " npm " , " install " , " --silent " ] ,
capture_output = True , text = True , cwd = str ( PROJECT_ROOT )
)
if result . returncode == 0 :
print_success ( " Node.js dependencies installed " )
else :
print_warning ( " npm install failed — run manually: cd ~/.hermes/hermes-agent && npm install " )
elif not node_modules . exists ( ) :
print_warning ( " Node.js not found — browser tools require: npm install (in the hermes-agent directory) " )
2026-02-02 19:01:51 -08:00
if api_key :
2026-02-23 23:06:47 +00:00
print_success ( " Configured ✓ " )
if " fal " in selected_keys :
print ( )
print ( color ( " ─── Image Generation (FAL) ─── " , Colors . CYAN ) )
print_info ( " Get your API key at: https://fal.ai/ " )
existing = get_env_value ( ' FAL_KEY ' )
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update API key? " , False ) :
api_key = prompt ( " FAL API key " , password = True )
if api_key :
save_env_value ( " FAL_KEY " , api_key )
print_success ( " Updated " )
else :
api_key = prompt ( " FAL API key " , password = True )
2026-02-02 19:28:27 -08:00
if api_key :
save_env_value ( " FAL_KEY " , api_key )
print_success ( " Configured ✓ " )
2026-02-04 09:36:51 -08:00
2026-02-23 23:06:47 +00:00
if " openai_voice " in selected_keys :
print ( )
print ( color ( " ─── Voice Transcription & TTS (OpenAI) ─── " , Colors . CYAN ) )
print_info ( " Used for Whisper speech-to-text and OpenAI TTS voices. " )
print_info ( " Get your API key at: https://platform.openai.com/api-keys " )
2026-02-23 23:21:33 +00:00
existing = get_env_value ( ' VOICE_TOOLS_OPENAI_KEY ' )
2026-02-23 23:06:47 +00:00
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update API key? " , False ) :
api_key = prompt ( " OpenAI API key " , password = True )
if api_key :
2026-02-23 23:21:33 +00:00
save_env_value ( " VOICE_TOOLS_OPENAI_KEY " , api_key )
2026-02-23 23:06:47 +00:00
print_success ( " Updated " )
else :
api_key = prompt ( " OpenAI API key " , password = True )
2026-02-12 10:05:08 -08:00
if api_key :
2026-02-23 23:21:33 +00:00
save_env_value ( " VOICE_TOOLS_OPENAI_KEY " , api_key )
2026-02-23 23:06:47 +00:00
print_success ( " Configured ✓ " )
if " elevenlabs " in selected_keys :
print ( )
print ( color ( " ─── Premium TTS (ElevenLabs) ─── " , Colors . CYAN ) )
print_info ( " High-quality voice synthesis. Free Edge TTS works without a key. " )
print_info ( " Get your API key at: https://elevenlabs.io/ " )
existing = get_env_value ( ' ELEVENLABS_API_KEY ' )
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update API key? " , False ) :
api_key = prompt ( " ElevenLabs API key " , password = True )
if api_key :
save_env_value ( " ELEVENLABS_API_KEY " , api_key )
print_success ( " Updated " )
else :
api_key = prompt ( " ElevenLabs API key " , password = True )
2026-02-12 10:05:08 -08:00
if api_key :
save_env_value ( " ELEVENLABS_API_KEY " , api_key )
print_success ( " Configured ✓ " )
2026-02-23 23:06:47 +00:00
if " rl_training " in selected_keys :
print ( )
print ( color ( " ─── RL Training (Tinker + WandB) ─── " , Colors . CYAN ) )
2026-02-04 09:36:51 -08:00
2026-02-23 23:06:47 +00:00
rl_python_ok = sys . version_info > = ( 3 , 11 )
if not rl_python_ok :
print_error ( f " Requires Python 3.11+ (current: { sys . version_info . major } . { sys . version_info . minor } ) " )
print_info ( " Upgrade Python and reinstall to enable RL training tools " )
else :
print_info ( " Get Tinker key at: https://tinker-console.thinkingmachines.ai/keys " )
print_info ( " Get WandB key at: https://wandb.ai/authorize " )
tinker_existing = get_env_value ( ' TINKER_API_KEY ' )
wandb_existing = get_env_value ( ' WANDB_API_KEY ' )
if tinker_existing and wandb_existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update credentials? " , False ) :
api_key = prompt ( " Tinker API key " , password = True )
if api_key :
save_env_value ( " TINKER_API_KEY " , api_key )
wandb_key = prompt ( " WandB API key " , password = True )
if wandb_key :
save_env_value ( " WANDB_API_KEY " , wandb_key )
print_success ( " Updated " )
2026-02-04 09:36:51 -08:00
else :
2026-02-07 00:05:04 +00:00
api_key = prompt ( " Tinker API key " , password = True )
if api_key :
save_env_value ( " TINKER_API_KEY " , api_key )
wandb_key = prompt ( " WandB API key " , password = True )
if wandb_key :
save_env_value ( " WANDB_API_KEY " , wandb_key )
2026-02-23 23:06:47 +00:00
# Auto-install tinker-atropos submodule if missing
2026-02-07 00:05:04 +00:00
try :
__import__ ( " tinker_atropos " )
except ImportError :
tinker_dir = PROJECT_ROOT / " tinker-atropos "
if tinker_dir . exists ( ) and ( tinker_dir / " pyproject.toml " ) . exists ( ) :
print_info ( " Installing tinker-atropos submodule... " )
import subprocess
2026-02-07 23:54:53 +00:00
import shutil
uv_bin = shutil . which ( " uv " )
if uv_bin :
result = subprocess . run (
[ uv_bin , " pip " , " install " , " -e " , str ( tinker_dir ) ] ,
capture_output = True , text = True
)
else :
result = subprocess . run (
[ sys . executable , " -m " , " pip " , " install " , " -e " , str ( tinker_dir ) ] ,
capture_output = True , text = True
)
2026-02-07 00:05:04 +00:00
if result . returncode == 0 :
print_success ( " tinker-atropos installed " )
else :
print_warning ( " tinker-atropos install failed — run manually: " )
2026-02-07 23:54:53 +00:00
print_info ( ' uv pip install -e " ./tinker-atropos " ' )
2026-02-07 00:05:04 +00:00
else :
print_warning ( " tinker-atropos submodule not found — run: " )
print_info ( " git submodule update --init --recursive " )
2026-02-07 23:54:53 +00:00
print_info ( ' uv pip install -e " ./tinker-atropos " ' )
2026-02-07 00:05:04 +00:00
if api_key and wandb_key :
print_success ( " Configured ✓ " )
else :
print_warning ( " Partially configured (both keys required) " )
2026-02-02 19:01:51 -08:00
2026-02-23 23:06:47 +00:00
if " github " in selected_keys :
print ( )
print ( color ( " ─── Skills Hub (GitHub) ─── " , Colors . CYAN ) )
print_info ( " Enables higher API rate limits for skill search/install " )
print_info ( " and publishing skills via GitHub PRs. " )
print_info ( " Get a token at: https://github.com/settings/tokens " )
existing = get_env_value ( ' GITHUB_TOKEN ' )
if existing :
print_success ( " Already configured ✓ " )
if prompt_yes_no ( " Update token? " , False ) :
token = prompt ( " GitHub Token (ghp_...) " , password = True )
if token :
save_env_value ( " GITHUB_TOKEN " , token )
print_success ( " Updated " )
else :
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
token = prompt ( " GitHub Token " , password = True )
if token :
save_env_value ( " GITHUB_TOKEN " , token )
print_success ( " Configured ✓ " )
2026-02-02 19:01:51 -08:00
# =========================================================================
2026-02-02 19:39:23 -08:00
# Save config and show summary
2026-02-02 19:01:51 -08:00
# =========================================================================
save_config ( config )
2026-02-02 19:39:23 -08:00
_print_setup_summary ( config , hermes_home )