feat(skills): /reload-skills slash command + skills_reload agent tool

Adds a public reload path for the in-process skill caches so newly
installed (or removed) skills become visible mid-session without a
gateway restart. Mirrors the shape of /reload-mcp.

Three surfaces:
* /reload-skills slash command — CLI (cli.py) and gateway (gateway/run.py),
  with /reload_skills alias for Telegram autocomplete and an explicit
  Discord registration.
* skills_reload agent tool (tools/skills_tool.py) — lets agents/subagents
  pick up freshly-installed skills via tool call.
* agent.skill_commands.reload_skills() — shared helper that clears
  _skill_commands, _SKILLS_PROMPT_CACHE (in-process LRU), and the
  on-disk .skills_prompt_snapshot.json, then returns an added/removed
  diff plus the new total count.

Tested:
* tests/agent/test_skill_commands_reload.py (9 cases)
* tests/cli/test_cli_reload_skills.py       (3 cases)
* tests/gateway/test_reload_skills_command.py (4 cases)

Use case: NemoClaw / OpenShell-style sandboxed orchestrators that drop
skills into ~/.hermes/skills mid-session, plus agentic flows where the
agent itself installs a skill via the shell tool and needs it bound
without a gateway restart. The Python helper
clear_skills_system_prompt_cache(clear_snapshot=True) already exists
internally — this PR just exposes it via slash command and tool.
This commit is contained in:
Shannon Sands 2026-04-29 13:58:45 +10:00 committed by Teknium
parent 113239f6e3
commit 7966560fb5
10 changed files with 682 additions and 4 deletions

View File

@ -284,6 +284,70 @@ def get_skill_commands() -> Dict[str, Dict[str, Any]]:
return _skill_commands
def reload_skills() -> Dict[str, Any]:
"""Re-scan the skills directory and invalidate every in-process skill cache.
Mirrors the ``/reload-mcp`` shape: clears state, rebuilds it, returns a
diff summary that the caller (CLI, gateway, or agent tool) can render
for the user / model.
What this clears:
* ``agent.skill_commands._skill_commands`` (slash-command map)
* ``agent.prompt_builder._SKILLS_PROMPT_CACHE`` (in-process LRU)
* ``.skills_prompt_snapshot.json`` on disk (cross-process snapshot)
What gets re-read on the next prompt build:
* ``~/.hermes/skills/`` and any ``skills.external_dirs``
* Plugin-provided skills
* ``skills.disabled`` / ``skills.platform_disabled`` from config.yaml
Returns:
Dict with keys::
{
"added": [skill names newly visible],
"removed": [skill names no longer visible],
"unchanged": [skill names present before and after],
"total": total skill count after rescan,
"commands": total /slash-skill count after rescan,
}
"""
# Snapshot pre-reload state from the cache (what the agent had been
# advertising). Comparing this to the post-rescan disk state shows
# the user/agent which skills actually appeared / disappeared.
before = set(_skill_commands.keys()) # /slash-form keys, e.g. "/demo"
# Clear the slash-command cache. ``scan_skill_commands`` already
# resets ``_skill_commands = {}`` internally, but we call the public
# rescan path so the result is observable to ``get_skill_commands``.
new_commands = scan_skill_commands()
# Clear the system-prompt cache (in-process LRU + on-disk snapshot)
# so the next prompt build re-walks the skills dir.
try:
from agent.prompt_builder import clear_skills_system_prompt_cache
clear_skills_system_prompt_cache(clear_snapshot=True)
except Exception as e: # pragma: no cover — best-effort
logger.debug("Could not clear skills prompt cache: %s", e)
after = set(new_commands.keys())
# Strip leading slash for display: callers compare bare skill names.
def _strip(s: set) -> set:
return {k.lstrip("/") for k in s}
added = sorted(_strip(after - before))
removed = sorted(_strip(before - after))
unchanged = sorted(_strip(after & before))
return {
"added": added,
"removed": removed,
"unchanged": unchanged,
"total": len(after),
"commands": len(new_commands),
}
def resolve_skill_command_key(command: str) -> Optional[str]:
"""Resolve a user-typed /command to its canonical skill_cmds key.

66
cli.py
View File

@ -3107,6 +3107,8 @@ class HermesCLI:
return "Processing skills command..."
if cmd_lower == "/reload-mcp":
return "Reloading MCP servers..."
if cmd_lower == "/reload-skills" or cmd_lower == "/reload_skills":
return "Reloading skills..."
if cmd_lower.startswith("/browser"):
return "Configuring browser..."
return "Processing command..."
@ -6286,6 +6288,9 @@ class HermesCLI:
elif canonical == "reload-mcp":
with self._busy_command(self._slow_command_status(cmd_original)):
self._reload_mcp()
elif canonical == "reload-skills":
with self._busy_command(self._slow_command_status(cmd_original)):
self._reload_skills()
elif canonical == "browser":
self._handle_browser_command(cmd_original)
elif canonical == "plugins":
@ -7497,6 +7502,67 @@ class HermesCLI:
except Exception as e:
print(f" ❌ MCP reload failed: {e}")
def _reload_skills(self) -> None:
"""Reload skills: rescan ~/.hermes/skills/, clear prompt cache.
Mirrors the ``/reload-mcp`` UX. After rescanning, the system prompt
for the next turn is rebuilt with the fresh skill list and any
``/skill-name`` slash commands are picked up immediately.
"""
try:
from agent.skill_commands import reload_skills
if not self._command_running:
print("🔄 Reloading skills...")
result = reload_skills()
added = result.get("added", [])
removed = result.get("removed", [])
total = result.get("total", 0)
if added:
print(f" Added: {', '.join(added)}")
if removed:
print(f" Removed: {', '.join(removed)}")
if not added and not removed:
print(" No changes detected.")
print(f" 📚 {total} skill(s) available")
# Inject a system-style note so the model sees the new skill
# list on its next turn. Appended at the end of history to
# preserve prompt-cache for the prefix.
change_parts = []
if added:
change_parts.append(f"Added skills: {', '.join(added)}")
if removed:
change_parts.append(f"Removed skills: {', '.join(removed)}")
if change_parts:
change_detail = ". ".join(change_parts) + ". "
self.conversation_history.append({
"role": "user",
"content": (
f"[IMPORTANT: Skills have been reloaded. {change_detail}"
f"{total} skill(s) now available. Use skills_list to "
f"see the updated catalog.]"
),
})
# Persist immediately so the session log reflects the
# reload event.
if self.agent is not None:
try:
self.agent._persist_session(
self.conversation_history,
self.conversation_history,
)
except Exception:
pass # Best-effort
print(f" ✅ Skill cache cleared")
except Exception as e:
print(f" ❌ Skills reload failed: {e}")
# ====================================================================
# Tool-call generation indicator (shown during streaming)
# ====================================================================

View File

@ -2270,6 +2270,10 @@ class DiscordAdapter(BasePlatformAdapter):
async def slash_reload_mcp(interaction: discord.Interaction):
await self._run_simple_slash(interaction, "/reload-mcp")
@tree.command(name="reload-skills", description="Re-scan ~/.hermes/skills/ for new or removed skills")
async def slash_reload_skills(interaction: discord.Interaction):
await self._run_simple_slash(interaction, "/reload-skills")
@tree.command(name="voice", description="Toggle voice reply mode")
@discord.app_commands.describe(mode="Voice mode: on, off, tts, channel, leave, or status")
@discord.app_commands.choices(mode=[

View File

@ -4219,6 +4219,9 @@ class GatewayRunner:
if canonical == "reload-mcp":
return await self._handle_reload_mcp_command(event)
if canonical == "reload-skills":
return await self._handle_reload_skills_command(event)
if canonical == "approve":
return await self._handle_approve_command(event)
@ -8208,6 +8211,58 @@ class GatewayRunner:
logger.warning("MCP reload failed: %s", e)
return f"❌ MCP reload failed: {e}"
async def _handle_reload_skills_command(self, event: MessageEvent) -> str:
"""Handle /reload-skills — re-scan skills dir and clear prompt cache."""
loop = asyncio.get_running_loop()
try:
from agent.skill_commands import reload_skills
result = await loop.run_in_executor(None, reload_skills)
added = result.get("added", [])
removed = result.get("removed", [])
total = result.get("total", 0)
lines = ["🔄 **Skills Reloaded**\n"]
if added:
lines.append(f" Added: {', '.join(added)}")
if removed:
lines.append(f" Removed: {', '.join(removed)}")
if not added and not removed:
lines.append("No changes detected.")
lines.append(f"\n📚 {total} skill(s) available")
# Inject a session-history note so the model sees the new skill
# list on its next turn. Appended after all existing messages
# to preserve prompt-cache for the prefix.
change_parts = []
if added:
change_parts.append(f"Added skills: {', '.join(added)}")
if removed:
change_parts.append(f"Removed skills: {', '.join(removed)}")
if change_parts:
change_detail = ". ".join(change_parts) + ". "
reload_msg = {
"role": "user",
"content": (
f"[IMPORTANT: Skills have been reloaded. {change_detail}"
f"{total} skill(s) now available. Use skills_list to "
f"see the updated catalog.]"
),
}
try:
session_entry = self.session_store.get_or_create_session(event.source)
self.session_store.append_to_transcript(
session_entry.session_id, reload_msg
)
except Exception:
pass # Best-effort; don't fail the reload over a transcript write
return "\n".join(lines)
except Exception as e:
logger.warning("Skills reload failed: %s", e)
return f"❌ Skills reload failed: {e}"
# ------------------------------------------------------------------
# /approve & /deny — explicit dangerous-command approval
# ------------------------------------------------------------------

View File

@ -155,6 +155,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
cli_only=True),
CommandDef("reload-mcp", "Reload MCP servers from config", "Tools & Skills",
aliases=("reload_mcp",)),
CommandDef("reload-skills", "Re-scan ~/.hermes/skills/ for newly installed or removed skills",
"Tools & Skills", aliases=("reload_skills",)),
CommandDef("browser", "Connect browser tools to your live Chrome via CDP", "Tools & Skills",
cli_only=True, args_hint="[connect|disconnect|status]",
subcommands=("connect", "disconnect", "status")),

View File

@ -0,0 +1,178 @@
"""Tests for ``agent.skill_commands.reload_skills`` and the ``skills_reload`` tool.
Covers the helper that powers ``/reload-skills`` (CLI + gateway slash command)
and the ``skills_reload`` agent tool both clear in-process skill caches and
return a diff of newly-visible / removed skill names.
"""
import json
import shutil
import tempfile
import textwrap
from pathlib import Path
import pytest
def _write_skill(skills_dir: Path, name: str, description: str = "") -> Path:
skill_dir = skills_dir / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
textwrap.dedent(
f"""\
---
name: {name}
description: {description or f'{name} skill'}
---
body
"""
)
)
return skill_dir
@pytest.fixture
def hermes_home(monkeypatch):
"""Isolate HERMES_HOME for ``reload_skills`` tests.
Rather than popping cache-bearing modules from ``sys.modules`` (which
races against pytest-xdist's parallel workers), we monkeypatch the
module-level ``HERMES_HOME`` / ``SKILLS_DIR`` constants in place so the
isolation is local to this fixture's scope.
"""
td = tempfile.mkdtemp(prefix="hermes-reload-skills-")
monkeypatch.setenv("HERMES_HOME", td)
home = Path(td)
(home / "skills").mkdir(parents=True, exist_ok=True)
# Import lazily (inside fixture) so the modules are already resident,
# then redirect their captured paths at the new temp dir.
import tools.skills_tool as _st
import agent.skill_commands as _sc
monkeypatch.setattr(_st, "HERMES_HOME", home, raising=False)
monkeypatch.setattr(_st, "SKILLS_DIR", home / "skills", raising=False)
# Reset the in-process slash-command cache so each test starts from zero.
monkeypatch.setattr(_sc, "_skill_commands", {}, raising=False)
yield home
shutil.rmtree(td, ignore_errors=True)
class TestReloadSkillsHelper:
"""``agent.skill_commands.reload_skills``."""
def test_returns_expected_keys(self, hermes_home):
from agent.skill_commands import reload_skills
result = reload_skills()
assert set(result) == {"added", "removed", "unchanged", "total", "commands"}
assert result["total"] == 0
assert result["added"] == []
assert result["removed"] == []
def test_detects_newly_added_skill(self, hermes_home):
from agent.skill_commands import reload_skills, get_skill_commands
# Prime the cache so subsequent diff is meaningful
get_skill_commands()
_write_skill(hermes_home / "skills", "demo")
result = reload_skills()
assert result["added"] == ["demo"]
assert result["removed"] == []
assert result["total"] == 1
assert result["commands"] == 1
def test_detects_removed_skill(self, hermes_home):
from agent.skill_commands import reload_skills
skill_dir = _write_skill(hermes_home / "skills", "demo")
# First reload: demo present
first = reload_skills()
assert first["total"] == 1
# Remove and reload
shutil.rmtree(skill_dir)
second = reload_skills()
assert second["removed"] == ["demo"]
assert second["added"] == []
assert second["total"] == 0
def test_clears_prompt_cache_snapshot(self, hermes_home):
"""The disk snapshot at ``.skills_prompt_snapshot.json`` must be removed."""
from agent.prompt_builder import _skills_prompt_snapshot_path
from agent.skill_commands import reload_skills
snapshot = _skills_prompt_snapshot_path()
snapshot.parent.mkdir(parents=True, exist_ok=True)
snapshot.write_text("{}")
assert snapshot.exists()
reload_skills()
assert not snapshot.exists(), "prompt cache snapshot should be removed"
def test_unchanged_skills_appear_in_unchanged_list(self, hermes_home):
from agent.skill_commands import reload_skills, get_skill_commands
_write_skill(hermes_home / "skills", "alpha")
# Prime cache
get_skill_commands()
# Call reload again with no FS changes
result = reload_skills()
assert "alpha" in result["unchanged"]
assert result["added"] == []
assert result["removed"] == []
class TestSkillsReloadTool:
"""``tools.skills_tool.skills_reload`` — the agent-facing tool."""
def test_tool_returns_json(self, hermes_home):
from tools.skills_tool import skills_reload
out = skills_reload()
result = json.loads(out)
assert result["success"] is True
assert set(result) == {
"success",
"added",
"removed",
"unchanged_count",
"total",
"commands",
}
def test_tool_reports_added_skill(self, hermes_home):
from agent.skill_commands import get_skill_commands
from tools.skills_tool import skills_reload
get_skill_commands() # prime cache
_write_skill(hermes_home / "skills", "freshly-added", "fresh skill")
result = json.loads(skills_reload())
assert result["success"] is True
assert result["added"] == ["freshly-added"]
assert result["total"] == 1
def test_tool_is_registered_in_skills_toolset(self, hermes_home):
# Importing the module triggers registry.register
import tools.skills_tool # noqa: F401
from tools.registry import registry
assert "skills_reload" in registry.get_tool_names_for_toolset("skills")
assert registry.get_toolset_for_tool("skills_reload") == "skills"
def test_tool_schema_has_no_required_args(self, hermes_home):
import tools.skills_tool # noqa: F401
from tools.registry import registry
schema = registry.get_schema("skills_reload")
assert schema["name"] == "skills_reload"
# Caller invokes with no arguments; tool returns the diff verbatim.
assert schema["parameters"].get("required", []) == []

View File

@ -0,0 +1,77 @@
"""Tests for the ``/reload-skills`` CLI slash command (``HermesCLI._reload_skills``)."""
from unittest.mock import MagicMock, patch
def _make_cli():
"""Build a minimal HermesCLI shell exposing ``_reload_skills``."""
import cli as cli_mod
obj = object.__new__(cli_mod.HermesCLI)
obj._command_running = False
obj.conversation_history = []
obj.agent = None
return obj
class TestReloadSkillsCLI:
def test_reports_added_and_removed(self, capsys):
cli = _make_cli()
with patch(
"agent.skill_commands.reload_skills",
return_value={
"added": ["alpha", "beta"],
"removed": ["gamma"],
"unchanged": ["delta"],
"total": 3,
"commands": 3,
},
):
cli._reload_skills()
out = capsys.readouterr().out
assert "Added: alpha, beta" in out
assert "Removed: gamma" in out
assert "3 skill(s) available" in out
# An informational message should be appended to the conversation
# so the model picks up the diff on the next turn.
assert len(cli.conversation_history) == 1
msg = cli.conversation_history[0]
assert msg["role"] == "user"
assert "Skills have been reloaded" in msg["content"]
assert "alpha" in msg["content"]
assert "gamma" in msg["content"]
def test_reports_no_changes(self, capsys):
cli = _make_cli()
with patch(
"agent.skill_commands.reload_skills",
return_value={
"added": [],
"removed": [],
"unchanged": ["alpha"],
"total": 1,
"commands": 1,
},
):
cli._reload_skills()
out = capsys.readouterr().out
assert "No changes detected" in out
assert "1 skill(s) available" in out
# Nothing changed — don't pollute history.
assert cli.conversation_history == []
def test_handles_reload_failure_gracefully(self, capsys):
cli = _make_cli()
with patch(
"agent.skill_commands.reload_skills",
side_effect=RuntimeError("boom"),
):
cli._reload_skills()
out = capsys.readouterr().out
assert "Skills reload failed" in out
assert "boom" in out
# Failure must not append a misleading "skills reloaded" note.
assert cli.conversation_history == []

View File

@ -0,0 +1,174 @@
"""Tests for the ``/reload-skills`` gateway slash command handler.
Verifies the gateway path that mirrors ``/reload-mcp``:
* dispatcher routes ``/reload-skills`` to ``_handle_reload_skills_command``
* the underscored alias ``/reload_skills`` is not flagged as unknown
* the handler invokes ``agent.skill_commands.reload_skills`` and renders a
human-readable diff
"""
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from gateway.config import GatewayConfig, Platform, PlatformConfig
from gateway.platforms.base import MessageEvent
from gateway.session import SessionEntry, SessionSource, build_session_key
def _make_source() -> SessionSource:
return SessionSource(
platform=Platform.TELEGRAM,
user_id="u1",
chat_id="c1",
user_name="tester",
chat_type="dm",
)
def _make_event(text: str) -> MessageEvent:
return MessageEvent(text=text, source=_make_source(), message_id="m1")
def _make_runner():
from gateway.run import GatewayRunner
runner = object.__new__(GatewayRunner)
runner.config = GatewayConfig(
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")}
)
adapter = MagicMock()
adapter.send = AsyncMock()
runner.adapters = {Platform.TELEGRAM: adapter}
runner._voice_mode = {}
runner.hooks = SimpleNamespace(
emit=AsyncMock(),
emit_collect=AsyncMock(return_value=[]),
loaded_hooks=False,
)
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-1",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner.session_store = MagicMock()
runner.session_store.get_or_create_session.return_value = session_entry
runner.session_store.load_transcript.return_value = []
runner.session_store.has_any_sessions.return_value = True
runner.session_store.append_to_transcript = MagicMock()
runner.session_store.rewrite_transcript = MagicMock()
runner.session_store.update_session = MagicMock()
runner._running_agents = {}
runner._pending_messages = {}
runner._pending_approvals = {}
runner._session_db = None
runner._reasoning_config = None
runner._provider_routing = {}
runner._fallback_model = None
runner._show_reasoning = False
runner._is_user_authorized = lambda _source: True
runner._set_session_env = lambda _context: None
runner._should_send_voice_reply = lambda *_args, **_kwargs: False
return runner
@pytest.mark.asyncio
async def test_reload_skills_handler_renders_added_and_removed(monkeypatch):
"""The handler should call ``reload_skills`` and surface the diff."""
import gateway.run as gateway_run
fake_result = {
"added": ["alpha", "beta"],
"removed": ["gamma"],
"unchanged": ["delta"],
"total": 3,
"commands": 3,
}
def _fake_reload_skills():
return fake_result
# Patch the symbol where ``_handle_reload_skills_command`` imports it from.
import agent.skill_commands as skill_commands_mod
monkeypatch.setattr(skill_commands_mod, "reload_skills", _fake_reload_skills)
runner = _make_runner()
out = await runner._handle_reload_skills_command(_make_event("/reload-skills"))
assert out is not None
assert "Skills Reloaded" in out
assert "alpha" in out and "beta" in out
assert "gamma" in out
assert "3 skill(s) available" in out
# A history note should be appended so the model sees the diff next turn.
runner.session_store.append_to_transcript.assert_called_once()
appended = runner.session_store.append_to_transcript.call_args[0][1]
assert appended["role"] == "user"
assert "Skills have been reloaded" in appended["content"]
@pytest.mark.asyncio
async def test_reload_skills_handler_reports_no_changes(monkeypatch):
"""When nothing changed, the handler should say so without injecting a note."""
import agent.skill_commands as skill_commands_mod
monkeypatch.setattr(
skill_commands_mod,
"reload_skills",
lambda: {
"added": [],
"removed": [],
"unchanged": ["alpha"],
"total": 1,
"commands": 1,
},
)
runner = _make_runner()
out = await runner._handle_reload_skills_command(_make_event("/reload-skills"))
assert "No changes detected" in out
assert "1 skill(s) available" in out
# No history note when nothing changed — preserves prompt cache.
runner.session_store.append_to_transcript.assert_not_called()
@pytest.mark.asyncio
async def test_dispatcher_routes_reload_skills(monkeypatch):
"""``/reload-skills`` must reach ``_handle_reload_skills_command``."""
import gateway.run as gateway_run
runner = _make_runner()
sentinel = "reload-skills handler reached"
runner._handle_reload_skills_command = AsyncMock(return_value=sentinel) # type: ignore[attr-defined]
monkeypatch.setattr(
gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}
)
result = await runner._handle_message(_make_event("/reload-skills"))
assert result == sentinel
@pytest.mark.asyncio
async def test_underscored_alias_not_flagged_unknown(monkeypatch):
"""Telegram autocomplete sends ``/reload_skills`` for ``/reload-skills``."""
import gateway.run as gateway_run
runner = _make_runner()
runner._handle_reload_skills_command = AsyncMock(return_value="ok") # type: ignore[attr-defined]
monkeypatch.setattr(
gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}
)
result = await runner._handle_message(_make_event("/reload_skills"))
if result is not None:
assert "Unknown command" not in result

View File

@ -1512,3 +1512,61 @@ registry.register(
check_fn=check_skills_requirements,
emoji="📚",
)
# ---------------------------------------------------------------------------
# skills_reload — rescan the skills dir without restarting the gateway
# ---------------------------------------------------------------------------
def skills_reload(task_id: str | None = None) -> str:
"""Re-scan ``~/.hermes/skills/`` and clear in-process skill caches.
Use this after installing a skill via the shell during a session so the
new skill becomes visible to ``skills_list`` / ``skill_view`` and the
skill catalogue in the system prompt without a gateway restart.
Returns:
JSON string with ``added``, ``removed``, ``unchanged``, ``total``,
and ``commands`` keys. ``added``/``removed`` are bare skill names
(no leading slash).
"""
try:
from agent.skill_commands import reload_skills as _reload
result = _reload()
except Exception as e:
return json.dumps({"success": False, "error": str(e)})
return json.dumps({
"success": True,
"added": result.get("added", []),
"removed": result.get("removed", []),
"unchanged_count": len(result.get("unchanged", [])),
"total": result.get("total", 0),
"commands": result.get("commands", 0),
})
SKILLS_RELOAD_SCHEMA = {
"name": "skills_reload",
"description": (
"Re-scan the skills directory and clear in-process skill caches. "
"Use after installing or removing a skill mid-session (e.g. via the "
"shell tool or skills_hub) so the new skill becomes visible to "
"skills_list / skill_view without restarting the gateway. Returns "
"the diff of added/removed skill names plus the new total count."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
}
registry.register(
name="skills_reload",
toolset="skills",
schema=SKILLS_RELOAD_SCHEMA,
handler=lambda args, **kw: skills_reload(task_id=kw.get("task_id")),
check_fn=check_skills_requirements,
emoji="🔄",
)

View File

@ -38,7 +38,7 @@ _HERMES_CORE_TOOLS = [
# Vision + image generation
"vision_analyze", "image_generate",
# Skills
"skills_list", "skill_view", "skill_manage",
"skills_list", "skill_view", "skill_manage", "skills_reload",
# Browser automation
"browser_navigate", "browser_snapshot", "browser_click",
"browser_type", "browser_scroll", "browser_back",
@ -105,7 +105,7 @@ TOOLSETS = {
"skills": {
"description": "Access, create, edit, and manage skill documents with specialized instructions and knowledge",
"tools": ["skills_list", "skill_view", "skill_manage"],
"tools": ["skills_list", "skill_view", "skill_manage", "skills_reload"],
"includes": []
},
@ -279,7 +279,7 @@ TOOLSETS = {
"terminal", "process",
"read_file", "write_file", "patch", "search_files",
"vision_analyze",
"skills_list", "skill_view", "skill_manage",
"skills_list", "skill_view", "skill_manage", "skills_reload",
"browser_navigate", "browser_snapshot", "browser_click",
"browser_type", "browser_scroll", "browser_back",
"browser_press", "browser_get_images",
@ -303,7 +303,7 @@ TOOLSETS = {
# Vision + image generation
"vision_analyze", "image_generate",
# Skills
"skills_list", "skill_view", "skill_manage",
"skills_list", "skill_view", "skill_manage", "skills_reload",
# Browser automation
"browser_navigate", "browser_snapshot", "browser_click",
"browser_type", "browser_scroll", "browser_back",