mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(plugins): add slash command registration for plugins (#2359)
Plugins can now register slash commands via ctx.register_command() in their register() function. Commands automatically appear in: - /help and COMMANDS_BY_CATEGORY (under 'Plugins' category) - Tab autocomplete in CLI - Telegram bot menu - Slack subcommand mapping - Gateway dispatch Handler signature: handler(args: str) -> str | None Async handlers are supported in gateway context. Changes: - commands.py: add register_plugin_command() and rebuild_lookups() - plugins.py: add register_command() to PluginContext, track in PluginManager._plugin_commands and LoadedPlugin.commands_registered - cli.py: dispatch plugin commands in process_command() - gateway/run.py: dispatch plugin commands before skill commands - tests: 5 new tests for registration, help, tracking, handler, gateway - docs: update plugins feature page and build guide
This commit is contained in:
parent
36079c6646
commit
8da410ed95
7 changed files with 326 additions and 2 deletions
|
|
@ -23,6 +23,12 @@ Tool registration
|
|||
-----------------
|
||||
``PluginContext.register_tool()`` delegates to ``tools.registry.register()``
|
||||
so plugin-defined tools appear alongside the built-in tools.
|
||||
|
||||
Slash command registration
|
||||
--------------------------
|
||||
``PluginContext.register_command()`` adds a slash command to the central
|
||||
``COMMAND_REGISTRY`` so it appears in /help, autocomplete, and gateway
|
||||
dispatch. Handlers receive the argument string and return a response.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -95,6 +101,7 @@ class LoadedPlugin:
|
|||
module: Optional[types.ModuleType] = None
|
||||
tools_registered: List[str] = field(default_factory=list)
|
||||
hooks_registered: List[str] = field(default_factory=list)
|
||||
commands_registered: List[str] = field(default_factory=list)
|
||||
enabled: bool = False
|
||||
error: Optional[str] = None
|
||||
|
||||
|
|
@ -141,6 +148,45 @@ class PluginContext:
|
|||
self._manager._plugin_tool_names.add(name)
|
||||
logger.debug("Plugin %s registered tool: %s", self.manifest.name, name)
|
||||
|
||||
# -- command registration ------------------------------------------------
|
||||
|
||||
def register_command(
|
||||
self,
|
||||
name: str,
|
||||
handler: Callable,
|
||||
description: str = "",
|
||||
aliases: tuple[str, ...] = (),
|
||||
args_hint: str = "",
|
||||
cli_only: bool = False,
|
||||
gateway_only: bool = False,
|
||||
) -> None:
|
||||
"""Register a slash command in the central command registry.
|
||||
|
||||
The *handler* is called with a single ``args`` string (everything
|
||||
after the command name) and should return a string to display to the
|
||||
user, or ``None`` for no output. Async handlers are also supported
|
||||
(they will be awaited in the gateway).
|
||||
|
||||
The command automatically appears in ``/help``, tab-autocomplete,
|
||||
Telegram bot menu, Slack subcommand mapping, and gateway dispatch.
|
||||
"""
|
||||
from hermes_cli.commands import CommandDef, register_plugin_command
|
||||
|
||||
cmd_def = CommandDef(
|
||||
name=name,
|
||||
description=description or f"Plugin command: {name}",
|
||||
category="Plugins",
|
||||
aliases=aliases,
|
||||
args_hint=args_hint,
|
||||
cli_only=cli_only,
|
||||
gateway_only=gateway_only,
|
||||
)
|
||||
register_plugin_command(cmd_def)
|
||||
self._manager._plugin_commands[name] = handler
|
||||
for alias in aliases:
|
||||
self._manager._plugin_commands[alias] = handler
|
||||
logger.debug("Plugin %s registered command: /%s", self.manifest.name, name)
|
||||
|
||||
# -- hook registration --------------------------------------------------
|
||||
|
||||
def register_hook(self, hook_name: str, callback: Callable) -> None:
|
||||
|
|
@ -172,6 +218,7 @@ class PluginManager:
|
|||
self._plugins: Dict[str, LoadedPlugin] = {}
|
||||
self._hooks: Dict[str, List[Callable]] = {}
|
||||
self._plugin_tool_names: Set[str] = set()
|
||||
self._plugin_commands: Dict[str, Callable] = {}
|
||||
self._discovered: bool = False
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -325,6 +372,14 @@ class PluginManager:
|
|||
for h in p.hooks_registered
|
||||
}
|
||||
)
|
||||
loaded.commands_registered = [
|
||||
c for c in self._plugin_commands
|
||||
if c not in {
|
||||
n
|
||||
for name, p in self._plugins.items()
|
||||
for n in p.commands_registered
|
||||
}
|
||||
]
|
||||
loaded.enabled = True
|
||||
|
||||
except Exception as exc:
|
||||
|
|
@ -420,6 +475,7 @@ class PluginManager:
|
|||
"enabled": loaded.enabled,
|
||||
"tools": len(loaded.tools_registered),
|
||||
"hooks": len(loaded.hooks_registered),
|
||||
"commands": len(loaded.commands_registered),
|
||||
"error": loaded.error,
|
||||
}
|
||||
)
|
||||
|
|
@ -454,3 +510,8 @@ def invoke_hook(hook_name: str, **kwargs: Any) -> None:
|
|||
def get_plugin_tool_names() -> Set[str]:
|
||||
"""Return the set of tool names registered by plugins."""
|
||||
return get_plugin_manager()._plugin_tool_names
|
||||
|
||||
|
||||
def get_plugin_command_handler(name: str) -> Optional[Callable]:
|
||||
"""Return the handler for a plugin-registered slash command, or None."""
|
||||
return get_plugin_manager()._plugin_commands.get(name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue