Compare commits

...

3 Commits

Author SHA1 Message Date
teknium1
b41de7ed7e feat: show activated skills before CLI banner 2026-03-14 19:29:28 -07:00
teknium1
62f5965650 test: cover continue with worktree and skills flags 2026-03-14 19:22:58 -07:00
teknium1
413037f6f6 feat: preload CLI skills on launch 2026-03-14 19:17:56 -07:00
7 changed files with 484 additions and 68 deletions

View File

@@ -14,6 +14,110 @@ logger = logging.getLogger(__name__)
_skill_commands: Dict[str, Dict[str, Any]] = {} _skill_commands: Dict[str, Dict[str, Any]] = {}
def _load_skill_payload(skill_identifier: str, task_id: str | None = None) -> tuple[dict[str, Any], Path | None, str] | None:
"""Load a skill by name/path and return (loaded_payload, skill_dir, display_name)."""
raw_identifier = (skill_identifier or "").strip()
if not raw_identifier:
return None
try:
from tools.skills_tool import SKILLS_DIR, skill_view
identifier_path = Path(raw_identifier).expanduser()
if identifier_path.is_absolute():
try:
normalized = str(identifier_path.resolve().relative_to(SKILLS_DIR.resolve()))
except Exception:
normalized = raw_identifier
else:
normalized = raw_identifier.lstrip("/")
loaded_skill = json.loads(skill_view(normalized, task_id=task_id))
except Exception:
return None
if not loaded_skill.get("success"):
return None
skill_name = str(loaded_skill.get("name") or normalized)
skill_path = str(loaded_skill.get("path") or "")
skill_dir = None
if skill_path:
try:
skill_dir = SKILLS_DIR / Path(skill_path).parent
except Exception:
skill_dir = None
return loaded_skill, skill_dir, skill_name
def _build_skill_message(
loaded_skill: dict[str, Any],
skill_dir: Path | None,
activation_note: str,
user_instruction: str = "",
) -> str:
"""Format a loaded skill into a user/system message payload."""
from tools.skills_tool import SKILLS_DIR
content = str(loaded_skill.get("content") or "")
parts = [activation_note, "", content.strip()]
if loaded_skill.get("setup_skipped"):
parts.extend(
[
"",
"[Skill setup note: Required environment setup was skipped. Continue loading the skill and explain any reduced functionality if it matters.]",
]
)
elif loaded_skill.get("gateway_setup_hint"):
parts.extend(
[
"",
f"[Skill setup note: {loaded_skill['gateway_setup_hint']}]",
]
)
elif loaded_skill.get("setup_needed") and loaded_skill.get("setup_note"):
parts.extend(
[
"",
f"[Skill setup note: {loaded_skill['setup_note']}]",
]
)
supporting = []
linked_files = loaded_skill.get("linked_files") or {}
for entries in linked_files.values():
if isinstance(entries, list):
supporting.extend(entries)
if not supporting and skill_dir:
for subdir in ("references", "templates", "scripts", "assets"):
subdir_path = skill_dir / subdir
if subdir_path.exists():
for f in sorted(subdir_path.rglob("*")):
if f.is_file():
rel = str(f.relative_to(skill_dir))
supporting.append(rel)
if supporting and skill_dir:
skill_view_target = str(skill_dir.relative_to(SKILLS_DIR))
parts.append("")
parts.append("[This skill has supporting files you can load with the skill_view tool:]")
for sf in supporting:
parts.append(f"- {sf}")
parts.append(
f'\nTo view any of these, use: skill_view(name="{skill_view_target}", file_path="<path>")'
)
if user_instruction:
parts.append("")
parts.append(f"The user has provided the following instruction alongside the skill invocation: {user_instruction}")
return "\n".join(parts)
def scan_skill_commands() -> Dict[str, Dict[str, Any]]: def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
"""Scan ~/.hermes/skills/ and return a mapping of /command -> skill info. """Scan ~/.hermes/skills/ and return a mapping of /command -> skill info.
@@ -83,77 +187,60 @@ def build_skill_invocation_message(
if not skill_info: if not skill_info:
return None return None
skill_name = skill_info["name"] loaded = _load_skill_payload(skill_info["skill_dir"], task_id=task_id)
skill_path = skill_info["skill_dir"] if not loaded:
return f"[Failed to load skill: {skill_info['name']}]"
try: loaded_skill, skill_dir, skill_name = loaded
from tools.skills_tool import SKILLS_DIR, skill_view activation_note = (
f'[SYSTEM: The user has invoked the "{skill_name}" skill, indicating they want '
"you to follow its instructions. The full skill content is loaded below.]"
)
return _build_skill_message(
loaded_skill,
skill_dir,
activation_note,
user_instruction=user_instruction,
)
loaded_skill = json.loads(skill_view(skill_path, task_id=task_id))
except Exception:
return f"[Failed to load skill: {skill_name}]"
if not loaded_skill.get("success"): def build_preloaded_skills_prompt(
return f"[Failed to load skill: {skill_name}]" skill_identifiers: list[str],
task_id: str | None = None,
) -> tuple[str, list[str], list[str]]:
"""Load one or more skills for session-wide CLI preloading.
content = str(loaded_skill.get("content") or "") Returns (prompt_text, loaded_skill_names, missing_identifiers).
skill_dir = Path(skill_info["skill_dir"]) """
prompt_parts: list[str] = []
loaded_names: list[str] = []
missing: list[str] = []
parts = [ seen: set[str] = set()
f'[SYSTEM: The user has invoked the "{skill_name}" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]', for raw_identifier in skill_identifiers:
"", identifier = (raw_identifier or "").strip()
content.strip(), if not identifier or identifier in seen:
] continue
seen.add(identifier)
if loaded_skill.get("setup_skipped"): loaded = _load_skill_payload(identifier, task_id=task_id)
parts.extend( if not loaded:
[ missing.append(identifier)
"", continue
"[Skill setup note: Required environment setup was skipped. Continue loading the skill and explain any reduced functionality if it matters.]",
] loaded_skill, skill_dir, skill_name = loaded
activation_note = (
f'[SYSTEM: The user launched this CLI session with the "{skill_name}" skill '
"preloaded. Treat its instructions as active guidance for the duration of this "
"session unless the user overrides them.]"
) )
elif loaded_skill.get("gateway_setup_hint"): prompt_parts.append(
parts.extend( _build_skill_message(
[ loaded_skill,
"", skill_dir,
f"[Skill setup note: {loaded_skill['gateway_setup_hint']}]", activation_note,
] )
)
elif loaded_skill.get("setup_needed") and loaded_skill.get("setup_note"):
parts.extend(
[
"",
f"[Skill setup note: {loaded_skill['setup_note']}]",
]
) )
loaded_names.append(skill_name)
supporting = [] return "\n\n".join(prompt_parts), loaded_names, missing
linked_files = loaded_skill.get("linked_files") or {}
for entries in linked_files.values():
if isinstance(entries, list):
supporting.extend(entries)
if not supporting:
for subdir in ("references", "templates", "scripts", "assets"):
subdir_path = skill_dir / subdir
if subdir_path.exists():
for f in sorted(subdir_path.rglob("*")):
if f.is_file():
rel = str(f.relative_to(skill_dir))
supporting.append(rel)
if supporting:
skill_view_target = str(Path(skill_path).relative_to(SKILLS_DIR))
parts.append("")
parts.append("[This skill has supporting files you can load with the skill_view tool:]")
for sf in supporting:
parts.append(f"- {sf}")
parts.append(
f'\nTo view any of these, use: skill_view(name="{skill_view_target}", file_path="<path>")'
)
if user_instruction:
parts.append("")
parts.append(f"The user has provided the following instruction alongside the skill invocation: {user_instruction}")
return "\n".join(parts)

60
cli.py
View File

@@ -8,6 +8,7 @@ Features ASCII art branding, interactive REPL, toolset selection, and rich forma
Usage: Usage:
python cli.py # Start interactive mode with all tools python cli.py # Start interactive mode with all tools
python cli.py --toolsets web,terminal # Start with specific toolsets python cli.py --toolsets web,terminal # Start with specific toolsets
python cli.py --skills hermes-agent-dev,github-auth
python cli.py -q "your question" # Single query mode python cli.py -q "your question" # Single query mode
python cli.py --list-tools # List available tools and exit python cli.py --list-tools # List available tools and exit
""" """
@@ -1043,11 +1044,40 @@ def build_welcome_banner(console: Console, model: str, cwd: str, tools: List[dic
# Skill Slash Commands — dynamic commands generated from installed skills # Skill Slash Commands — dynamic commands generated from installed skills
# ============================================================================ # ============================================================================
from agent.skill_commands import scan_skill_commands, get_skill_commands, build_skill_invocation_message from agent.skill_commands import (
scan_skill_commands,
get_skill_commands,
build_skill_invocation_message,
build_preloaded_skills_prompt,
)
_skill_commands = scan_skill_commands() _skill_commands = scan_skill_commands()
def _parse_skills_argument(skills: str | list[str] | tuple[str, ...] | None) -> list[str]:
"""Normalize a CLI skills flag into a deduplicated list of skill identifiers."""
if not skills:
return []
if isinstance(skills, str):
raw_values = [skills]
elif isinstance(skills, (list, tuple)):
raw_values = [str(item) for item in skills if item is not None]
else:
raw_values = [str(skills)]
parsed: list[str] = []
seen: set[str] = set()
for raw in raw_values:
for part in raw.split(","):
normalized = part.strip()
if not normalized or normalized in seen:
continue
seen.add(normalized)
parsed.append(normalized)
return parsed
def save_config_value(key_path: str, value: any) -> bool: def save_config_value(key_path: str, value: any) -> bool:
""" """
Save a value to the active config file at the specified key path. Save a value to the active config file at the specified key path.
@@ -1313,6 +1343,8 @@ class HermesCLI:
self._command_status = "" self._command_status = ""
self._attached_images: list[Path] = [] self._attached_images: list[Path] = []
self._image_counter = 0 self._image_counter = 0
self.preloaded_skills: list[str] = []
self._startup_skills_line_shown = False
# Voice mode state (also reinitialized inside run() for interactive TUI). # Voice mode state (also reinitialized inside run() for interactive TUI).
self._voice_lock = threading.Lock() self._voice_lock = threading.Lock()
@@ -1599,6 +1631,13 @@ class HermesCLI:
def show_banner(self): def show_banner(self):
"""Display the welcome banner in Claude Code style.""" """Display the welcome banner in Claude Code style."""
self.console.clear() self.console.clear()
if self.preloaded_skills and not self._startup_skills_line_shown:
skills_label = ", ".join(self.preloaded_skills)
self.console.print(
f"[bold {_accent_hex()}]Activated skills:[/] {skills_label}"
)
self.console.print()
self._startup_skills_line_shown = True
# Auto-compact for narrow terminals — the full banner with caduceus # Auto-compact for narrow terminals — the full banner with caduceus
# + tool list needs ~80 columns minimum to render without wrapping. # + tool list needs ~80 columns minimum to render without wrapping.
@@ -5829,6 +5868,7 @@ def main(
query: str = None, query: str = None,
q: str = None, q: str = None,
toolsets: str = None, toolsets: str = None,
skills: str | list[str] | tuple[str, ...] = None,
model: str = None, model: str = None,
provider: str = None, provider: str = None,
api_key: str = None, api_key: str = None,
@@ -5853,6 +5893,7 @@ def main(
query: Single query to execute (then exit). Alias: -q query: Single query to execute (then exit). Alias: -q
q: Shorthand for --query q: Shorthand for --query
toolsets: Comma-separated list of toolsets to enable (e.g., "web,terminal") toolsets: Comma-separated list of toolsets to enable (e.g., "web,terminal")
skills: Comma-separated or repeated list of skills to preload for the session
model: Model to use (default: anthropic/claude-opus-4-20250514) model: Model to use (default: anthropic/claude-opus-4-20250514)
provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn") provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn")
api_key: API key for authentication api_key: API key for authentication
@@ -5869,6 +5910,7 @@ def main(
Examples: Examples:
python cli.py # Start interactive mode python cli.py # Start interactive mode
python cli.py --toolsets web,terminal # Use specific toolsets python cli.py --toolsets web,terminal # Use specific toolsets
python cli.py --skills hermes-agent-dev,github-auth
python cli.py -q "What is Python?" # Single query mode python cli.py -q "What is Python?" # Single query mode
python cli.py --list-tools # List tools and exit python cli.py --list-tools # List tools and exit
python cli.py --resume 20260225_143052_a1b2c3 # Resume session python cli.py --resume 20260225_143052_a1b2c3 # Resume session
@@ -5938,6 +5980,8 @@ def main(
else: else:
toolsets_list = ["hermes-cli"] toolsets_list = ["hermes-cli"]
parsed_skills = _parse_skills_argument(skills)
# Create CLI instance # Create CLI instance
cli = HermesCLI( cli = HermesCLI(
model=model, model=model,
@@ -5953,6 +5997,20 @@ def main(
pass_session_id=pass_session_id, pass_session_id=pass_session_id,
) )
if parsed_skills:
skills_prompt, loaded_skills, missing_skills = build_preloaded_skills_prompt(
parsed_skills,
task_id=cli.session_id,
)
if missing_skills:
missing_display = ", ".join(missing_skills)
raise ValueError(f"Unknown skill(s): {missing_display}")
if skills_prompt:
cli.system_prompt = "\n\n".join(
part for part in (cli.system_prompt, skills_prompt) if part
).strip()
cli.preloaded_skills = loaded_skills
# Inject worktree context into agent's system prompt # Inject worktree context into agent's system prompt
if wt_info: if wt_info:
wt_note = ( wt_note = (

View File

@@ -499,6 +499,7 @@ def cmd_chat(args):
"model": args.model, "model": args.model,
"provider": getattr(args, "provider", None), "provider": getattr(args, "provider", None),
"toolsets": args.toolsets, "toolsets": args.toolsets,
"skills": getattr(args, "skills", None),
"verbose": args.verbose, "verbose": args.verbose,
"quiet": getattr(args, "quiet", False), "quiet": getattr(args, "quiet", False),
"query": args.query, "query": args.query,
@@ -510,7 +511,11 @@ def cmd_chat(args):
# Filter out None values # Filter out None values
kwargs = {k: v for k, v in kwargs.items() if v is not None} kwargs = {k: v for k, v in kwargs.items() if v is not None}
cli_main(**kwargs) try:
cli_main(**kwargs)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
def cmd_gateway(args): def cmd_gateway(args):
@@ -2268,6 +2273,7 @@ Examples:
hermes config edit Edit config in $EDITOR hermes config edit Edit config in $EDITOR
hermes config set model gpt-4 Set a config value hermes config set model gpt-4 Set a config value
hermes gateway Run messaging gateway hermes gateway Run messaging gateway
hermes -s hermes-agent-dev,github-auth
hermes -w Start in isolated git worktree hermes -w Start in isolated git worktree
hermes gateway install Install as system service hermes gateway install Install as system service
hermes sessions list List past sessions hermes sessions list List past sessions
@@ -2306,6 +2312,12 @@ For more help on a command:
default=False, default=False,
help="Run in an isolated git worktree (for parallel agents)" help="Run in an isolated git worktree (for parallel agents)"
) )
parser.add_argument(
"--skills", "-s",
action="append",
default=None,
help="Preload one or more skills for the session (repeat flag or comma-separate)"
)
parser.add_argument( parser.add_argument(
"--yolo", "--yolo",
action="store_true", action="store_true",
@@ -2341,6 +2353,12 @@ For more help on a command:
"-t", "--toolsets", "-t", "--toolsets",
help="Comma-separated toolsets to enable" help="Comma-separated toolsets to enable"
) )
chat_parser.add_argument(
"-s", "--skills",
action="append",
default=None,
help="Preload one or more skills for the session (repeat flag or comma-separate)"
)
chat_parser.add_argument( chat_parser.add_argument(
"--provider", "--provider",
choices=["auto", "openrouter", "nous", "openai-codex", "anthropic", "zai", "kimi-coding", "minimax", "minimax-cn"], choices=["auto", "openrouter", "nous", "openai-codex", "anthropic", "zai", "kimi-coding", "minimax", "minimax-cn"],

View File

@@ -4,7 +4,11 @@ import os
from unittest.mock import patch from unittest.mock import patch
import tools.skills_tool as skills_tool_module import tools.skills_tool as skills_tool_module
from agent.skill_commands import scan_skill_commands, build_skill_invocation_message from agent.skill_commands import (
scan_skill_commands,
build_skill_invocation_message,
build_preloaded_skills_prompt,
)
def _make_skill( def _make_skill(
@@ -79,6 +83,33 @@ class TestScanSkillCommands:
assert "/generic-tool" in result assert "/generic-tool" in result
class TestBuildPreloadedSkillsPrompt:
def test_builds_prompt_for_multiple_named_skills(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "first-skill")
_make_skill(tmp_path, "second-skill")
prompt, loaded, missing = build_preloaded_skills_prompt(
["first-skill", "second-skill"]
)
assert missing == []
assert loaded == ["first-skill", "second-skill"]
assert "first-skill" in prompt
assert "second-skill" in prompt
assert "preloaded" in prompt.lower()
def test_reports_missing_named_skills(self, tmp_path):
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
_make_skill(tmp_path, "present-skill")
prompt, loaded, missing = build_preloaded_skills_prompt(
["present-skill", "missing-skill"]
)
assert "present-skill" in prompt
assert loaded == ["present-skill"]
assert missing == ["missing-skill"]
class TestBuildSkillInvocationMessage: class TestBuildSkillInvocationMessage:
def test_loads_skill_by_stored_path_when_frontmatter_name_differs(self, tmp_path): def test_loads_skill_by_stored_path_when_frontmatter_name_differs(self, tmp_path):
skill_dir = tmp_path / "mlops" / "audiocraft" skill_dir = tmp_path / "mlops" / "audiocraft"

View File

@@ -0,0 +1,77 @@
import sys
def test_top_level_skills_flag_defaults_to_chat(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["skills"] = args.skills
captured["command"] = args.command
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "-s", "hermes-agent-dev,github-auth"],
)
main_mod.main()
assert captured == {
"skills": ["hermes-agent-dev,github-auth"],
"command": None,
}
def test_chat_subcommand_accepts_skills_flag(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["skills"] = args.skills
captured["query"] = args.query
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "chat", "-s", "github-auth", "-q", "hello"],
)
main_mod.main()
assert captured == {
"skills": ["github-auth"],
"query": "hello",
}
def test_continue_worktree_and_skills_flags_work_together(monkeypatch):
import hermes_cli.main as main_mod
captured = {}
def fake_cmd_chat(args):
captured["continue_last"] = args.continue_last
captured["worktree"] = args.worktree
captured["skills"] = args.skills
captured["command"] = args.command
monkeypatch.setattr(main_mod, "cmd_chat", fake_cmd_chat)
monkeypatch.setattr(
sys,
"argv",
["hermes", "-c", "-w", "-s", "hermes-agent-dev"],
)
main_mod.main()
assert captured == {
"continue_last": True,
"worktree": True,
"skills": ["hermes-agent-dev"],
"command": "chat",
}

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import importlib
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
def _make_real_cli(**kwargs):
clean_config = {
"model": {
"default": "anthropic/claude-opus-4.6",
"base_url": "https://openrouter.ai/api/v1",
"provider": "auto",
},
"display": {"compact": False, "tool_progress": "all"},
"agent": {},
"terminal": {"env_type": "local"},
}
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
prompt_toolkit_stubs = {
"prompt_toolkit": MagicMock(),
"prompt_toolkit.history": MagicMock(),
"prompt_toolkit.styles": MagicMock(),
"prompt_toolkit.patch_stdout": MagicMock(),
"prompt_toolkit.application": MagicMock(),
"prompt_toolkit.layout": MagicMock(),
"prompt_toolkit.layout.processors": MagicMock(),
"prompt_toolkit.filters": MagicMock(),
"prompt_toolkit.layout.dimension": MagicMock(),
"prompt_toolkit.layout.menus": MagicMock(),
"prompt_toolkit.widgets": MagicMock(),
"prompt_toolkit.key_binding": MagicMock(),
"prompt_toolkit.completion": MagicMock(),
"prompt_toolkit.formatted_text": MagicMock(),
}
with patch.dict(sys.modules, prompt_toolkit_stubs), patch.dict(
"os.environ", clean_env, clear=False
):
import cli as cli_mod
cli_mod = importlib.reload(cli_mod)
with patch.object(cli_mod, "get_tool_definitions", return_value=[]), patch.dict(
cli_mod.__dict__, {"CLI_CONFIG": clean_config}
):
return cli_mod.HermesCLI(**kwargs)
class _DummyCLI:
def __init__(self, **kwargs):
self.kwargs = kwargs
self.session_id = "session-123"
self.system_prompt = "base prompt"
self.preloaded_skills = []
def show_banner(self):
return None
def show_tools(self):
return None
def show_toolsets(self):
return None
def run(self):
return None
def test_main_applies_preloaded_skills_to_system_prompt(monkeypatch):
import cli as cli_mod
created = {}
def fake_cli(**kwargs):
created["cli"] = _DummyCLI(**kwargs)
return created["cli"]
monkeypatch.setattr(cli_mod, "HermesCLI", fake_cli)
monkeypatch.setattr(
cli_mod,
"build_preloaded_skills_prompt",
lambda skills, task_id=None: ("skill prompt", ["hermes-agent-dev", "github-auth"], []),
)
with pytest.raises(SystemExit):
cli_mod.main(skills="hermes-agent-dev,github-auth", list_tools=True)
cli_obj = created["cli"]
assert cli_obj.system_prompt == "base prompt\n\nskill prompt"
assert cli_obj.preloaded_skills == ["hermes-agent-dev", "github-auth"]
def test_main_raises_for_unknown_preloaded_skill(monkeypatch):
import cli as cli_mod
monkeypatch.setattr(cli_mod, "HermesCLI", lambda **kwargs: _DummyCLI(**kwargs))
monkeypatch.setattr(
cli_mod,
"build_preloaded_skills_prompt",
lambda skills, task_id=None: ("", [], ["missing-skill"]),
)
with pytest.raises(ValueError, match=r"Unknown skill\(s\): missing-skill"):
cli_mod.main(skills="missing-skill", list_tools=True)
def test_show_banner_prints_preloaded_skills_once_before_banner():
cli_obj = _make_real_cli(compact=False)
cli_obj.preloaded_skills = ["hermes-agent-dev", "github-auth"]
cli_obj.console = MagicMock()
with patch("cli.build_welcome_banner") as mock_banner, patch(
"shutil.get_terminal_size", return_value=os.terminal_size((120, 40))
):
cli_obj.show_banner()
cli_obj.show_banner()
print_calls = [
call.args[0]
for call in cli_obj.console.print.call_args_list
if call.args and isinstance(call.args[0], str)
]
startup_lines = [line for line in print_calls if "Activated skills:" in line]
assert len(startup_lines) == 1
assert "Activated skills:" in startup_lines[0]
assert "hermes-agent-dev, github-auth" in startup_lines[0]
assert mock_banner.call_count == 2

View File

@@ -27,6 +27,10 @@ hermes chat --provider openrouter # Force OpenRouter
# With specific toolsets # With specific toolsets
hermes chat --toolsets "web,terminal,skills" hermes chat --toolsets "web,terminal,skills"
# Start with one or more skills preloaded
hermes -s hermes-agent-dev,github-auth
hermes chat -s github-pr-workflow -q "open a draft PR"
# Resume previous sessions # Resume previous sessions
hermes --continue # Resume the most recent CLI session (-c) hermes --continue # Resume the most recent CLI session (-c)
hermes --resume <session_id> # Resume a specific session by ID (-r) hermes --resume <session_id> # Resume a specific session by ID (-r)
@@ -121,6 +125,17 @@ quick_commands:
Then type `/status` or `/gpu` in any chat. See the [Configuration guide](/docs/user-guide/configuration#quick-commands) for more examples. Then type `/status` or `/gpu` in any chat. See the [Configuration guide](/docs/user-guide/configuration#quick-commands) for more examples.
## Preloading Skills at Launch
If you already know which skills you want active for the session, pass them at launch time:
```bash
hermes -s hermes-agent-dev,github-auth
hermes chat -s github-pr-workflow -s github-auth
```
Hermes loads each named skill into the session prompt before the first turn. The same flag works in interactive mode and single-query mode.
## Skill Slash Commands ## Skill Slash Commands
Every installed skill in `~/.hermes/skills/` is automatically registered as a slash command. The skill name becomes the command: Every installed skill in `~/.hermes/skills/` is automatically registered as a slash command. The skill name becomes the command: