mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
1 Commits
fix/plugin
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05601e1f03 |
@@ -3779,6 +3779,16 @@ For more help on a command:
|
|||||||
|
|
||||||
plugins_subparsers.add_parser("list", aliases=["ls"], help="List installed plugins")
|
plugins_subparsers.add_parser("list", aliases=["ls"], help="List installed plugins")
|
||||||
|
|
||||||
|
plugins_enable = plugins_subparsers.add_parser(
|
||||||
|
"enable", help="Enable a disabled plugin"
|
||||||
|
)
|
||||||
|
plugins_enable.add_argument("name", help="Plugin name to enable")
|
||||||
|
|
||||||
|
plugins_disable = plugins_subparsers.add_parser(
|
||||||
|
"disable", help="Disable a plugin without removing it"
|
||||||
|
)
|
||||||
|
plugins_disable.add_argument("name", help="Plugin name to disable")
|
||||||
|
|
||||||
def cmd_plugins(args):
|
def cmd_plugins(args):
|
||||||
from hermes_cli.plugins_cmd import plugins_command
|
from hermes_cli.plugins_cmd import plugins_command
|
||||||
plugins_command(args)
|
plugins_command(args)
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ def _env_enabled(name: str) -> bool:
|
|||||||
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_disabled_plugins() -> set:
|
||||||
|
"""Read the disabled plugins list from config.yaml."""
|
||||||
|
try:
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
config = load_config()
|
||||||
|
disabled = config.get("plugins", {}).get("disabled", [])
|
||||||
|
return set(disabled) if isinstance(disabled, list) else set()
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Data classes
|
# Data classes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -199,8 +210,15 @@ class PluginManager:
|
|||||||
# 3. Pip / entry-point plugins
|
# 3. Pip / entry-point plugins
|
||||||
manifests.extend(self._scan_entry_points())
|
manifests.extend(self._scan_entry_points())
|
||||||
|
|
||||||
# Load each manifest
|
# Load each manifest (skip user-disabled plugins)
|
||||||
|
disabled = _get_disabled_plugins()
|
||||||
for manifest in manifests:
|
for manifest in manifests:
|
||||||
|
if manifest.name in disabled:
|
||||||
|
loaded = LoadedPlugin(manifest=manifest, enabled=False)
|
||||||
|
loaded.error = "disabled via config"
|
||||||
|
self._plugins[manifest.name] = loaded
|
||||||
|
logger.debug("Skipping disabled plugin '%s'", manifest.name)
|
||||||
|
continue
|
||||||
self._load_plugin(manifest)
|
self._load_plugin(manifest)
|
||||||
|
|
||||||
if manifests:
|
if manifests:
|
||||||
|
|||||||
@@ -374,6 +374,73 @@ def cmd_remove(name: str) -> None:
|
|||||||
_display_removed(name, plugins_dir)
|
_display_removed(name, plugins_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_disabled_set() -> set:
|
||||||
|
"""Read the disabled plugins set from config.yaml."""
|
||||||
|
try:
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
config = load_config()
|
||||||
|
disabled = config.get("plugins", {}).get("disabled", [])
|
||||||
|
return set(disabled) if isinstance(disabled, list) else set()
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _save_disabled_set(disabled: set) -> None:
|
||||||
|
"""Write the disabled plugins list to config.yaml."""
|
||||||
|
from hermes_cli.config import load_config, save_config
|
||||||
|
config = load_config()
|
||||||
|
if "plugins" not in config:
|
||||||
|
config["plugins"] = {}
|
||||||
|
config["plugins"]["disabled"] = sorted(disabled)
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_enable(name: str) -> None:
|
||||||
|
"""Enable a previously disabled plugin."""
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
plugins_dir = _plugins_dir()
|
||||||
|
|
||||||
|
# Verify the plugin exists
|
||||||
|
target = plugins_dir / name
|
||||||
|
if not target.is_dir():
|
||||||
|
console.print(f"[red]Plugin '{name}' is not installed.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
disabled = _get_disabled_set()
|
||||||
|
if name not in disabled:
|
||||||
|
console.print(f"[dim]Plugin '{name}' is already enabled.[/dim]")
|
||||||
|
return
|
||||||
|
|
||||||
|
disabled.discard(name)
|
||||||
|
_save_disabled_set(disabled)
|
||||||
|
console.print(f"[green]✓[/green] Plugin [bold]{name}[/bold] enabled. Takes effect on next session.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_disable(name: str) -> None:
|
||||||
|
"""Disable a plugin without removing it."""
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
plugins_dir = _plugins_dir()
|
||||||
|
|
||||||
|
# Verify the plugin exists
|
||||||
|
target = plugins_dir / name
|
||||||
|
if not target.is_dir():
|
||||||
|
console.print(f"[red]Plugin '{name}' is not installed.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
disabled = _get_disabled_set()
|
||||||
|
if name in disabled:
|
||||||
|
console.print(f"[dim]Plugin '{name}' is already disabled.[/dim]")
|
||||||
|
return
|
||||||
|
|
||||||
|
disabled.add(name)
|
||||||
|
_save_disabled_set(disabled)
|
||||||
|
console.print(f"[yellow]⊘[/yellow] Plugin [bold]{name}[/bold] disabled. Takes effect on next session.")
|
||||||
|
|
||||||
|
|
||||||
def cmd_list() -> None:
|
def cmd_list() -> None:
|
||||||
"""List installed plugins."""
|
"""List installed plugins."""
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
@@ -393,8 +460,11 @@ def cmd_list() -> None:
|
|||||||
console.print("[dim]Install with:[/dim] hermes plugins install owner/repo")
|
console.print("[dim]Install with:[/dim] hermes plugins install owner/repo")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
disabled = _get_disabled_set()
|
||||||
|
|
||||||
table = Table(title="Installed Plugins", show_lines=False)
|
table = Table(title="Installed Plugins", show_lines=False)
|
||||||
table.add_column("Name", style="bold")
|
table.add_column("Name", style="bold")
|
||||||
|
table.add_column("Status")
|
||||||
table.add_column("Version", style="dim")
|
table.add_column("Version", style="dim")
|
||||||
table.add_column("Description")
|
table.add_column("Description")
|
||||||
table.add_column("Source", style="dim")
|
table.add_column("Source", style="dim")
|
||||||
@@ -420,11 +490,86 @@ def cmd_list() -> None:
|
|||||||
if (d / ".git").exists():
|
if (d / ".git").exists():
|
||||||
source = "git"
|
source = "git"
|
||||||
|
|
||||||
table.add_row(name, str(version), description, source)
|
is_disabled = name in disabled or d.name in disabled
|
||||||
|
status = "[red]disabled[/red]" if is_disabled else "[green]enabled[/green]"
|
||||||
|
table.add_row(name, status, str(version), description, source)
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
console.print(table)
|
console.print(table)
|
||||||
console.print()
|
console.print()
|
||||||
|
console.print("[dim]Interactive toggle:[/dim] hermes plugins")
|
||||||
|
console.print("[dim]Enable/disable:[/dim] hermes plugins enable/disable <name>")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_toggle() -> None:
|
||||||
|
"""Interactive curses checklist to enable/disable installed plugins."""
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
yaml = None
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
plugins_dir = _plugins_dir()
|
||||||
|
|
||||||
|
dirs = sorted(d for d in plugins_dir.iterdir() if d.is_dir())
|
||||||
|
if not dirs:
|
||||||
|
console.print("[dim]No plugins installed.[/dim]")
|
||||||
|
console.print("[dim]Install with:[/dim] hermes plugins install owner/repo")
|
||||||
|
return
|
||||||
|
|
||||||
|
disabled = _get_disabled_set()
|
||||||
|
|
||||||
|
# Build items list: "name — description" for display
|
||||||
|
names = []
|
||||||
|
labels = []
|
||||||
|
selected = set()
|
||||||
|
|
||||||
|
for i, d in enumerate(dirs):
|
||||||
|
manifest_file = d / "plugin.yaml"
|
||||||
|
name = d.name
|
||||||
|
description = ""
|
||||||
|
|
||||||
|
if manifest_file.exists() and yaml:
|
||||||
|
try:
|
||||||
|
with open(manifest_file) as f:
|
||||||
|
manifest = yaml.safe_load(f) or {}
|
||||||
|
name = manifest.get("name", d.name)
|
||||||
|
description = manifest.get("description", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
names.append(name)
|
||||||
|
label = f"{name} — {description}" if description else name
|
||||||
|
labels.append(label)
|
||||||
|
|
||||||
|
if name not in disabled and d.name not in disabled:
|
||||||
|
selected.add(i)
|
||||||
|
|
||||||
|
from hermes_cli.curses_ui import curses_checklist
|
||||||
|
|
||||||
|
result = curses_checklist(
|
||||||
|
title="Plugins — toggle enabled/disabled",
|
||||||
|
items=labels,
|
||||||
|
selected=selected,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute new disabled set from deselected items
|
||||||
|
new_disabled = set()
|
||||||
|
for i, name in enumerate(names):
|
||||||
|
if i not in result:
|
||||||
|
new_disabled.add(name)
|
||||||
|
|
||||||
|
if new_disabled != disabled:
|
||||||
|
_save_disabled_set(new_disabled)
|
||||||
|
enabled_count = len(names) - len(new_disabled)
|
||||||
|
console.print(
|
||||||
|
f"\n[green]✓[/green] {enabled_count} enabled, {len(new_disabled)} disabled. "
|
||||||
|
f"Takes effect on next session."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print("\n[dim]No changes.[/dim]")
|
||||||
|
|
||||||
|
|
||||||
def plugins_command(args) -> None:
|
def plugins_command(args) -> None:
|
||||||
@@ -437,8 +582,14 @@ def plugins_command(args) -> None:
|
|||||||
cmd_update(args.name)
|
cmd_update(args.name)
|
||||||
elif action in ("remove", "rm", "uninstall"):
|
elif action in ("remove", "rm", "uninstall"):
|
||||||
cmd_remove(args.name)
|
cmd_remove(args.name)
|
||||||
elif action in ("list", "ls") or action is None:
|
elif action == "enable":
|
||||||
|
cmd_enable(args.name)
|
||||||
|
elif action == "disable":
|
||||||
|
cmd_disable(args.name)
|
||||||
|
elif action in ("list", "ls"):
|
||||||
cmd_list()
|
cmd_list()
|
||||||
|
elif action is None:
|
||||||
|
cmd_toggle()
|
||||||
else:
|
else:
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
|
|||||||
@@ -399,17 +399,22 @@ See [MCP Config Reference](./mcp-config-reference.md) and [Use MCP with Hermes](
|
|||||||
## `hermes plugins`
|
## `hermes plugins`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
hermes plugins <subcommand>
|
hermes plugins [subcommand]
|
||||||
```
|
```
|
||||||
|
|
||||||
Manage Hermes Agent plugins.
|
Manage Hermes Agent plugins. Running `hermes plugins` with no subcommand launches an interactive curses checklist to enable/disable installed plugins.
|
||||||
|
|
||||||
| Subcommand | Description |
|
| Subcommand | Description |
|
||||||
|------------|-------------|
|
|------------|-------------|
|
||||||
|
| *(none)* | Interactive toggle UI — enable/disable plugins with arrow keys and space. |
|
||||||
| `install <identifier> [--force]` | Install a plugin from a Git URL or `owner/repo`. |
|
| `install <identifier> [--force]` | Install a plugin from a Git URL or `owner/repo`. |
|
||||||
| `update <name>` | Pull latest changes for an installed plugin. |
|
| `update <name>` | Pull latest changes for an installed plugin. |
|
||||||
| `remove <name>` (aliases: `rm`, `uninstall`) | Remove an installed plugin. |
|
| `remove <name>` (aliases: `rm`, `uninstall`) | Remove an installed plugin. |
|
||||||
| `list` (alias: `ls`) | List installed plugins. |
|
| `enable <name>` | Enable a disabled plugin. |
|
||||||
|
| `disable <name>` | Disable a plugin without removing it. |
|
||||||
|
| `list` (alias: `ls`) | List installed plugins with enabled/disabled status. |
|
||||||
|
|
||||||
|
Disabled plugins are stored in `config.yaml` under `plugins.disabled` and skipped during loading.
|
||||||
|
|
||||||
See [Plugins](../user-guide/features/plugins.md) and [Build a Hermes Plugin](../guides/build-a-hermes-plugin.md).
|
See [Plugins](../user-guide/features/plugins.md) and [Build a Hermes Plugin](../guides/build-a-hermes-plugin.md).
|
||||||
|
|
||||||
|
|||||||
@@ -87,9 +87,26 @@ The handler receives the argument string (everything after `/greet`) and returns
|
|||||||
|
|
||||||
## Managing plugins
|
## Managing plugins
|
||||||
|
|
||||||
```
|
```bash
|
||||||
/plugins # list loaded plugins in a session
|
hermes plugins # interactive toggle UI — enable/disable with checkboxes
|
||||||
hermes config set display.show_cost true # show cost in status bar
|
hermes plugins list # table view with enabled/disabled status
|
||||||
|
hermes plugins install user/repo # install from Git
|
||||||
|
hermes plugins update my-plugin # pull latest
|
||||||
|
hermes plugins remove my-plugin # uninstall
|
||||||
|
hermes plugins enable my-plugin # re-enable a disabled plugin
|
||||||
|
hermes plugins disable my-plugin # disable without removing
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Running `hermes plugins` with no arguments launches an interactive curses checklist (same UI as `hermes tools`) where you can toggle plugins on/off with arrow keys and space.
|
||||||
|
|
||||||
|
Disabled plugins remain installed but are skipped during loading. The disabled list is stored in `config.yaml` under `plugins.disabled`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugins:
|
||||||
|
disabled:
|
||||||
|
- my-noisy-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
In a running session, `/plugins` shows which plugins are currently loaded.
|
||||||
|
|
||||||
See the **[full guide](/docs/guides/build-a-hermes-plugin)** for handler contracts, schema format, hook behavior, error handling, and common mistakes.
|
See the **[full guide](/docs/guides/build-a-hermes-plugin)** for handler contracts, schema format, hook behavior, error handling, and common mistakes.
|
||||||
|
|||||||
Reference in New Issue
Block a user