/provider command (CLI + gateway):
Shows all providers with auth status (✓/✗), aliases, and active marker.
Users can now discover what provider names work with provider:model syntax.
Gateway bugs fixed:
- Config was saved even when validation.persist=False (told user 'session
only' but actually persisted the unvalidated model)
- HERMES_INFERENCE_PROVIDER env var not set on provider switch, causing
the switch to be silently overridden if that env var was already set
parse_model_input hardened:
- Colon only treated as provider delimiter if left side is a recognized
provider name or alias. 'anthropic/claude-3.5-sonnet:beta' now passes
through as a model name instead of trying provider='anthropic/claude-3.5-sonnet'.
- HTTP URLs, random colons no longer misinterpreted.
56 tests passing across model validation, CLI commands, and integration.
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""Slash command definitions and autocomplete for the Hermes CLI.
|
|
|
|
Contains the shared built-in ``COMMANDS`` dict and ``SlashCommandCompleter``.
|
|
The completer can optionally include dynamic skill slash commands supplied by the
|
|
interactive CLI.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from typing import Any
|
|
|
|
from prompt_toolkit.completion import Completer, Completion
|
|
|
|
|
|
COMMANDS = {
|
|
"/help": "Show this help message",
|
|
"/tools": "List available tools",
|
|
"/toolsets": "List available toolsets",
|
|
"/model": "Show or change the current model",
|
|
"/provider": "Show available providers and current provider",
|
|
"/prompt": "View/set custom system prompt",
|
|
"/personality": "Set a predefined personality",
|
|
"/clear": "Clear screen and reset conversation (fresh start)",
|
|
"/history": "Show conversation history",
|
|
"/new": "Start a new conversation (reset history)",
|
|
"/reset": "Reset conversation only (keep screen)",
|
|
"/retry": "Retry the last message (resend to agent)",
|
|
"/undo": "Remove the last user/assistant exchange",
|
|
"/save": "Save the current conversation",
|
|
"/config": "Show current configuration",
|
|
"/cron": "Manage scheduled tasks (list, add, remove)",
|
|
"/skills": "Search, install, inspect, or manage skills from online registries",
|
|
"/platforms": "Show gateway/messaging platform status",
|
|
"/verbose": "Cycle tool progress display: off → new → all → verbose",
|
|
"/compress": "Manually compress conversation context (flush memories + summarize)",
|
|
"/usage": "Show token usage for the current session",
|
|
"/insights": "Show usage insights and analytics (last 30 days)",
|
|
"/paste": "Check clipboard for an image and attach it",
|
|
"/reload-mcp": "Reload MCP servers from config.yaml",
|
|
"/quit": "Exit the CLI (also: /exit, /q)",
|
|
}
|
|
|
|
|
|
class SlashCommandCompleter(Completer):
|
|
"""Autocomplete for built-in slash commands and optional skill commands."""
|
|
|
|
def __init__(
|
|
self,
|
|
skill_commands_provider: Callable[[], Mapping[str, dict[str, Any]]] | None = None,
|
|
) -> None:
|
|
self._skill_commands_provider = skill_commands_provider
|
|
|
|
def _iter_skill_commands(self) -> Mapping[str, dict[str, Any]]:
|
|
if self._skill_commands_provider is None:
|
|
return {}
|
|
try:
|
|
return self._skill_commands_provider() or {}
|
|
except Exception:
|
|
return {}
|
|
|
|
@staticmethod
|
|
def _completion_text(cmd_name: str, word: str) -> str:
|
|
"""Return replacement text for a completion.
|
|
|
|
When the user has already typed the full command exactly (``/help``),
|
|
returning ``help`` would be a no-op and prompt_toolkit suppresses the
|
|
menu. Appending a trailing space keeps the dropdown visible and makes
|
|
backspacing retrigger it naturally.
|
|
"""
|
|
return f"{cmd_name} " if cmd_name == word else cmd_name
|
|
|
|
def get_completions(self, document, complete_event):
|
|
text = document.text_before_cursor
|
|
if not text.startswith("/"):
|
|
return
|
|
|
|
word = text[1:]
|
|
|
|
for cmd, desc in COMMANDS.items():
|
|
cmd_name = cmd[1:]
|
|
if cmd_name.startswith(word):
|
|
yield Completion(
|
|
self._completion_text(cmd_name, word),
|
|
start_position=-len(word),
|
|
display=cmd,
|
|
display_meta=desc,
|
|
)
|
|
|
|
for cmd, info in self._iter_skill_commands().items():
|
|
cmd_name = cmd[1:]
|
|
if cmd_name.startswith(word):
|
|
description = str(info.get("description", "Skill command"))
|
|
short_desc = description[:50] + ("..." if len(description) > 50 else "")
|
|
yield Completion(
|
|
self._completion_text(cmd_name, word),
|
|
start_position=-len(word),
|
|
display=cmd,
|
|
display_meta=f"⚡ {short_desc}",
|
|
)
|