Merge branch 'main' into feat/honcho-integration
This commit is contained in:
commit
4a9086b848
73 changed files with 7080 additions and 280 deletions
62
cli.py
62
cli.py
|
|
@ -201,7 +201,7 @@ def load_cli_config() -> Dict[str, Any]:
|
|||
"max_tool_calls": 50, # Max RPC tool calls per execution
|
||||
},
|
||||
"delegation": {
|
||||
"max_iterations": 25, # Max tool-calling turns per child agent
|
||||
"max_iterations": 45, # Max tool-calling turns per child agent
|
||||
"default_toolsets": ["terminal", "file", "web"], # Default toolsets for subagents
|
||||
},
|
||||
}
|
||||
|
|
@ -286,6 +286,7 @@ def load_cli_config() -> Dict[str, Any]:
|
|||
"container_memory": "TERMINAL_CONTAINER_MEMORY",
|
||||
"container_disk": "TERMINAL_CONTAINER_DISK",
|
||||
"container_persistent": "TERMINAL_CONTAINER_PERSISTENT",
|
||||
"docker_volumes": "TERMINAL_DOCKER_VOLUMES",
|
||||
# Sudo support (works with all backends)
|
||||
"sudo_password": "SUDO_PASSWORD",
|
||||
}
|
||||
|
|
@ -298,7 +299,12 @@ def load_cli_config() -> Dict[str, Any]:
|
|||
for config_key, env_var in env_mappings.items():
|
||||
if config_key in terminal_config:
|
||||
if _file_has_terminal_config or env_var not in os.environ:
|
||||
os.environ[env_var] = str(terminal_config[config_key])
|
||||
val = terminal_config[config_key]
|
||||
if isinstance(val, list):
|
||||
import json
|
||||
os.environ[env_var] = json.dumps(val)
|
||||
else:
|
||||
os.environ[env_var] = str(val)
|
||||
|
||||
# Apply browser config to environment variables
|
||||
browser_config = defaults.get("browser", {})
|
||||
|
|
@ -400,6 +406,29 @@ def _cprint(text: str):
|
|||
"""
|
||||
_pt_print(_PT_ANSI(text))
|
||||
|
||||
|
||||
class ChatConsole:
|
||||
"""Rich Console adapter for prompt_toolkit's patch_stdout context.
|
||||
|
||||
Captures Rich's rendered ANSI output and routes it through _cprint
|
||||
so colors and markup render correctly inside the interactive chat loop.
|
||||
Drop-in replacement for Rich Console — just pass this to any function
|
||||
that expects a console.print() interface.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
from io import StringIO
|
||||
self._buffer = StringIO()
|
||||
self._inner = Console(file=self._buffer, force_terminal=True, highlight=False)
|
||||
|
||||
def print(self, *args, **kwargs):
|
||||
self._buffer.seek(0)
|
||||
self._buffer.truncate()
|
||||
self._inner.print(*args, **kwargs)
|
||||
output = self._buffer.getvalue()
|
||||
for line in output.rstrip("\n").split("\n"):
|
||||
_cprint(line)
|
||||
|
||||
# ASCII Art - HERMES-AGENT logo (full width, single line - requires ~95 char terminal)
|
||||
HERMES_AGENT_LOGO = """[bold #FFD700]██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗[/]
|
||||
[bold #FFD700]██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝[/]
|
||||
|
|
@ -708,7 +737,7 @@ def save_config_value(key_path: str, value: any) -> bool:
|
|||
keys = key_path.split('.')
|
||||
current = config
|
||||
for key in keys[:-1]:
|
||||
if key not in current:
|
||||
if key not in current or not isinstance(current[key], dict):
|
||||
current[key] = {}
|
||||
current = current[key]
|
||||
current[keys[-1]] = value
|
||||
|
|
@ -742,14 +771,14 @@ class HermesCLI:
|
|||
provider: str = None,
|
||||
api_key: str = None,
|
||||
base_url: str = None,
|
||||
max_turns: int = 60,
|
||||
max_turns: int = None,
|
||||
verbose: bool = False,
|
||||
compact: bool = False,
|
||||
resume: str = None,
|
||||
):
|
||||
"""
|
||||
Initialize the Hermes CLI.
|
||||
|
||||
|
||||
Args:
|
||||
model: Model to use (default: from env or claude-sonnet)
|
||||
toolsets: List of toolsets to enable (default: all)
|
||||
|
|
@ -792,7 +821,7 @@ class HermesCLI:
|
|||
self._nous_key_expires_at: Optional[str] = None
|
||||
self._nous_key_source: Optional[str] = None
|
||||
# Max turns priority: CLI arg > env var > config file (agent.max_turns or root max_turns) > default
|
||||
if max_turns != 60: # CLI arg was explicitly set
|
||||
if max_turns is not None:
|
||||
self.max_turns = max_turns
|
||||
elif os.getenv("HERMES_MAX_ITERATIONS"):
|
||||
self.max_turns = int(os.getenv("HERMES_MAX_ITERATIONS"))
|
||||
|
|
@ -1140,7 +1169,12 @@ class HermesCLI:
|
|||
terminal_cwd = os.getenv("TERMINAL_CWD", os.getcwd())
|
||||
terminal_timeout = os.getenv("TERMINAL_TIMEOUT", "60")
|
||||
|
||||
config_path = Path(__file__).parent / 'cli-config.yaml'
|
||||
user_config_path = Path.home() / '.hermes' / 'config.yaml'
|
||||
project_config_path = Path(__file__).parent / 'cli-config.yaml'
|
||||
if user_config_path.exists():
|
||||
config_path = user_config_path
|
||||
else:
|
||||
config_path = project_config_path
|
||||
config_status = "(loaded)" if config_path.exists() else "(not found)"
|
||||
|
||||
api_key_display = '********' + self.api_key[-4:] if self.api_key and len(self.api_key) > 4 else 'Not set!'
|
||||
|
|
@ -1172,7 +1206,7 @@ class HermesCLI:
|
|||
print()
|
||||
print(" -- Session --")
|
||||
print(f" Started: {self.session_start.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f" Config File: cli-config.yaml {config_status}")
|
||||
print(f" Config File: {config_path} {config_status}")
|
||||
print()
|
||||
|
||||
def show_history(self):
|
||||
|
|
@ -1517,7 +1551,7 @@ class HermesCLI:
|
|||
def _handle_skills_command(self, cmd: str):
|
||||
"""Handle /skills slash command — delegates to hermes_cli.skills_hub."""
|
||||
from hermes_cli.skills_hub import handle_skills_slash
|
||||
handle_skills_slash(cmd, self.console)
|
||||
handle_skills_slash(cmd, ChatConsole())
|
||||
|
||||
def _show_gateway_status(self):
|
||||
"""Show status of the gateway and connected messaging platforms."""
|
||||
|
|
@ -2226,13 +2260,17 @@ class HermesCLI:
|
|||
|
||||
# Paste collapsing: detect large pastes and save to temp file
|
||||
_paste_counter = [0]
|
||||
_prev_text_len = [0]
|
||||
|
||||
def _on_text_changed(buf):
|
||||
"""Detect large pastes and collapse them to a file reference."""
|
||||
text = buf.text
|
||||
line_count = text.count('\n')
|
||||
# Heuristic: if text jumps to 5+ lines in one change, it's a paste
|
||||
if line_count >= 5 and not text.startswith('/'):
|
||||
chars_added = len(text) - _prev_text_len[0]
|
||||
_prev_text_len[0] = len(text)
|
||||
# Heuristic: a real paste adds many characters at once (not just a
|
||||
# single newline from Alt+Enter) AND the result has 5+ lines.
|
||||
if line_count >= 5 and chars_added > 1 and not text.startswith('/'):
|
||||
_paste_counter[0] += 1
|
||||
# Save to temp file
|
||||
paste_dir = Path(os.path.expanduser("~/.hermes/pastes"))
|
||||
|
|
@ -2643,7 +2681,7 @@ def main(
|
|||
provider: str = None,
|
||||
api_key: str = None,
|
||||
base_url: str = None,
|
||||
max_turns: int = 60,
|
||||
max_turns: int = None,
|
||||
verbose: bool = False,
|
||||
compact: bool = False,
|
||||
list_tools: bool = False,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue