This commit is contained in:
hhh7666 2026-04-24 19:25:52 -05:00 committed by GitHub
commit 6067ca917b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 947 additions and 0 deletions

View file

@ -0,0 +1,820 @@
"""
Hermes Group Commands - Multi-agent group chat functionality.
Usage:
hermes group agents - List all registered agents
hermes group register <name> - Register an agent from profile
hermes group unregister <name> - Unregister an agent
hermes group create <name> <agents> - Create a group
hermes group list - List all groups
hermes group delete <name> - Delete a group
hermes group info <name> - Show group details
hermes group add <group> <agent> - Add agent to group
hermes group remove <group> <agent> - Remove agent from group
hermes group chat <name> - Start group chat
hermes group continue <file> - Continue from memory file
hermes group memory - List memory files
"""
from __future__ import annotations
import asyncio
import json
import os
import signal
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
import yaml
# Global flag for interrupt handling
_interrupted = False
_original_sigint_handler = None
# Terminal utilities
try:
from hermes_cli.curses_ui import flush_stdin
except ImportError:
import termios
def flush_stdin():
"""Flush any stray bytes from stdin (only when needed)."""
try:
if sys.stdin.isatty():
termios.tcflush(sys.stdin, termios.TCIFLUSH)
except Exception:
pass
def _sigint_handler(signum, frame):
"""Custom SIGINT handler that sets a flag instead of terminating."""
global _interrupted
_interrupted = True
def _setup_readline() -> None:
"""Setup readline for proper line editing."""
try:
import readline
# Set up basic editing bindings for cross-platform compatibility
readline.parse_and_bind("tab: complete")
readline.parse_and_bind(r'"\e[3~": delete-char')
readline.parse_and_bind(r'"\e[2~": quoted-insert')
readline.parse_and_bind(r'"\e[H": beginning-of-line')
readline.parse_and_bind(r'"\e[F": end-of-line')
readline.parse_and_bind(r'"\e[1~": beginning-of-line')
readline.parse_and_bind(r'"\e[4~": end-of-line')
# Enable vi/emacs mode (emacs is more common and intuitive for most)
readline.parse_and_bind("set editing-mode emacs")
# History settings
readline.set_history_length(100)
except ImportError:
pass
except Exception:
pass
def _get_input(prompt: str = "You: ") -> str:
"""Get user input with proper line editing support."""
_setup_readline()
try:
return input(prompt).strip()
except (KeyboardInterrupt, EOFError):
raise
# Lazy imports for optional packages - loaded inside functions to avoid hard deps
# =============================================================================
# Constants
# =============================================================================
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
# Profiles are always in ~/.hermes/profiles/ (root level), not in HERMES_HOME
HERMES_ROOT = Path.home() / ".hermes"
PROFILES_DIR = HERMES_ROOT / "profiles"
# Group data is stored in the root hermes directory (shared across profiles)
GROUPS_DIR = HERMES_ROOT / "groups"
AGENTS_REGISTRY = HERMES_ROOT / "agents" / "registry.yaml"
MEMORY_DIR = HERMES_ROOT / "group_memory"
# Ensure directories exist
GROUPS_DIR.mkdir(parents=True, exist_ok=True)
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
AGENTS_REGISTRY.parent.mkdir(parents=True, exist_ok=True)
# =============================================================================
# Agent Registry
# =============================================================================
def load_agent_registry() -> dict[str, dict[str, Any]]:
"""Load agent registry from YAML file."""
if not AGENTS_REGISTRY.exists():
return {}
try:
with open(AGENTS_REGISTRY, "r") as f:
data = yaml.safe_load(f)
return data.get("agents", {}) if data else {}
except Exception:
return {}
def save_agent_registry(registry: dict[str, dict[str, Any]]) -> None:
"""Save agent registry to YAML file."""
with open(AGENTS_REGISTRY, "w") as f:
yaml.dump({"agents": registry}, f, default_flow_style=False)
def list_agents() -> list[dict[str, Any]]:
"""List all registered agents."""
registry = load_agent_registry()
return [
{"name": name, **info}
for name, info in registry.items()
]
def register_agent(name: str, profile: str | None = None) -> bool:
"""Register an agent from a profile."""
registry = load_agent_registry()
# Check if profile exists (profiles are in ~/.hermes/profiles/)
profile_path = PROFILES_DIR / (profile or name)
if not profile_path.exists():
print(f"Error: Profile '{profile or name}' does not exist")
print(f"Available profiles:")
if PROFILES_DIR.exists():
for p in sorted(PROFILES_DIR.iterdir()):
if p.is_dir():
print(f" - {p.name}")
return False
# Get soul.md content as system prompt
soul_path = profile_path / "SOUL.md"
system_message = ""
if soul_path.exists():
system_message = soul_path.read_text()
registry[name] = {
"profile": profile or name,
"registered_at": datetime.now().isoformat(),
"system_message": system_message if system_message else f"You are {name}.",
}
save_agent_registry(registry)
return True
def unregister_agent(name: str) -> bool:
"""Unregister an agent."""
registry = load_agent_registry()
if name not in registry:
print(f"Error: Agent '{name}' is not registered")
return False
del registry[name]
save_agent_registry(registry)
return True
# =============================================================================
# Group Management
# =============================================================================
def load_groups_config() -> dict[str, dict[str, Any]]:
"""Load groups configuration."""
config_file = GROUPS_DIR / "config.yaml"
if not config_file.exists():
return {"groups": {}}
try:
with open(config_file, "r") as f:
return yaml.safe_load(f) or {"groups": {}}
except Exception:
return {"groups": {}}
def save_groups_config(config: dict[str, dict[str, Any]]) -> None:
"""Save groups configuration."""
config_file = GROUPS_DIR / "config.yaml"
with open(config_file, "w") as f:
yaml.dump(config, f, default_flow_style=False)
def create_group(name: str, agents: list[str]) -> bool:
"""Create a new group with specified agents."""
if len(agents) < 2:
print("Error: A group must have at least 2 agents")
return False
registry = load_agent_registry()
# Verify all agents are registered
for agent in agents:
if agent not in registry:
print(f"Error: Agent '{agent}' is not registered")
print("Run 'hermes group agents' to see registered agents")
return False
config = load_groups_config()
if name in config.get("groups", {}):
print(f"Error: Group '{name}' already exists")
return False
if "groups" not in config:
config["groups"] = {}
config["groups"][name] = {
"agents": agents,
"created_at": datetime.now().isoformat(),
"last_chat": None,
}
save_groups_config(config)
print(f"Group '{name}' created with agents: {', '.join(agents)}")
return True
def list_groups() -> list[dict[str, Any]]:
"""List all groups."""
config = load_groups_config()
groups = config.get("groups", {})
return [
{
"name": name,
"agents": info.get("agents", []),
"created_at": info.get("created_at", ""),
"last_chat": info.get("last_chat"),
}
for name, info in groups.items()
]
def delete_group(name: str) -> bool:
"""Delete a group."""
config = load_groups_config()
if name not in config.get("groups", {}):
print(f"Error: Group '{name}' does not exist")
return False
del config["groups"][name]
save_groups_config(config)
print(f"Group '{name}' deleted")
return True
def show_group_info(name: str) -> dict[str, Any] | None:
"""Show detailed information about a group."""
config = load_groups_config()
group = config.get("groups", {}).get(name)
if not group:
print(f"Error: Group '{name}' does not exist")
return None
return {"name": name, **group}
def add_agent_to_group(group_name: str, agent_name: str) -> bool:
"""Add an agent to an existing group."""
registry = load_agent_registry()
if agent_name not in registry:
print(f"Error: Agent '{agent_name}' is not registered")
return False
config = load_groups_config()
if group_name not in config.get("groups", {}):
print(f"Error: Group '{group_name}' does not exist")
return False
agents = config["groups"][group_name].get("agents", [])
if agent_name in agents:
print(f"Agent '{agent_name}' is already in group '{group_name}'")
return False
agents.append(agent_name)
config["groups"][group_name]["agents"] = agents
save_groups_config(config)
print(f"Agent '{agent_name}' added to group '{group_name}'")
return True
def remove_agent_from_group(group_name: str, agent_name: str) -> bool:
"""Remove an agent from a group."""
config = load_groups_config()
if group_name not in config.get("groups", {}):
print(f"Error: Group '{group_name}' does not exist")
return False
agents = config["groups"][group_name].get("agents", [])
if agent_name not in agents:
print(f"Error: Agent '{agent_name}' is not in group '{group_name}'")
return False
if len(agents) <= 2:
print("Error: A group must have at least 2 agents")
return False
agents.remove(agent_name)
config["groups"][group_name]["agents"] = agents
save_groups_config(config)
print(f"Agent '{agent_name}' removed from group '{group_name}'")
return True
# =============================================================================
# Memory System
# =============================================================================
def get_memory_filename(group_name: str) -> Path:
"""Generate a memory filename for a group."""
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
return MEMORY_DIR / f"{timestamp}_{group_name}.jsonl"
def save_message_to_memory(filename: Path, message: dict[str, Any]) -> None:
"""Save a message to the memory file."""
with open(filename, "a") as f:
f.write(json.dumps(message, ensure_ascii=False) + "\n")
def load_memory_file(filename: Path) -> list[dict[str, Any]]:
"""Load messages from a memory file."""
if not filename.exists():
return []
messages = []
with open(filename, "r") as f:
for line in f:
line = line.strip()
if line:
try:
messages.append(json.loads(line))
except json.JSONDecodeError:
pass
return messages
def list_memory_files() -> list[dict[str, Any]]:
"""List all memory files."""
if not MEMORY_DIR.exists():
return []
files = []
for f in sorted(MEMORY_DIR.iterdir()):
if f.suffix == ".jsonl":
stat = f.stat()
files.append({
"name": f.name,
"path": str(f),
"size": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
})
return files
# =============================================================================
# Group Chat (using autogen-agentchat 0.7.x)
# =============================================================================
def get_llm_config() -> dict[str, str]:
"""Get LLM configuration from config.yaml."""
config_file = HERMES_ROOT / "config.yaml"
if not config_file.exists():
return {}
try:
import yaml
with open(config_file, "r") as f:
config = yaml.safe_load(f)
model_config = config.get("model", {})
return {
"model": model_config.get("default", "deepseek-v3"),
"api_key": model_config.get("api_key", ""),
"base_url": model_config.get("base_url", "https://api.newcoin.top/v1"),
}
except Exception:
return {}
def run_group_chat(group_name: str, continue_file: Path | None = None) -> None:
"""Run an interactive group chat."""
# Reset terminal state before any input() - crucial for Chinese IME
import os
os.system("stty sane 2>/dev/null")
# Reset interrupted flag
global _interrupted
_interrupted = False
try:
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_core.models import ModelInfo
from autogen_ext.models.openai import OpenAIChatCompletionClient
except ImportError as e:
print("Error: autogen packages are required for group chat")
print(f"Import error: {e}")
print("Install with: pip install autogen-agentchat autogen-ext --break-system-packages")
return
config = load_groups_config()
group = config.get("groups", {}).get(group_name)
if not group:
print(f"Error: Group '{group_name}' does not exist")
return
agents_list = group.get("agents", [])
registry = load_agent_registry()
# Get LLM config from config.yaml
llm_config = get_llm_config()
# Fallback to environment variables
model = llm_config.get("model") or os.environ.get("HERMES_GROUP_MODEL", "deepseek-v3")
api_key = llm_config.get("api_key") or os.environ.get("DEEPSEEK_API_KEY", "")
base_url = llm_config.get("base_url") or os.environ.get("DEEPSEEK_BASE_URL", "https://api.newcoin.top/v1")
if not api_key:
print("Error: No API key configured")
print("Please set up your model in Hermes first: hermes model")
return
# Create model client
from autogen_core.models import ModelInfo
from autogen_ext.models.openai import OpenAIChatCompletionClient
model_client = OpenAIChatCompletionClient(
model=model,
api_key=api_key,
base_url=base_url,
model_info=ModelInfo(
family="deepseek",
vision=False,
function_calling=True,
json_output=True,
structured_output=False,
multiple_system_messages=True,
),
)
# Create agents (without user proxy - we'll handle input manually)
participants = []
for agent_name in agents_list:
agent_info = registry.get(agent_name, {})
system_msg = agent_info.get(
"system_message",
f"You are {agent_name}."
)
agent = AssistantAgent(
name=agent_name,
model_client=model_client,
system_message=system_msg,
)
participants.append(agent)
# Create group chat WITHOUT user - we handle input manually
chat = RoundRobinGroupChat(
participants=participants,
max_turns=50,
)
# Memory file for this session
memory_file = get_memory_filename(group_name)
print("\n" + "=" * 60)
print(f"GROUP CHAT: {group_name}")
print(f"Agents: {', '.join(agents_list)}")
print("Type 'exit' to end | 'stop' to interrupt | 'save' to save")
print("=" * 60 + "\n")
# Save session start
save_message_to_memory(memory_file, {
"type": "session_start",
"group": group_name,
"agents": agents_list,
"timestamp": datetime.now().isoformat(),
})
# Interactive chat loop - manually send messages to group chat
has_messages = False
current_task = None
def _handle_interrupt(signum, frame):
"""Handle Ctrl+C: cancel current task and break loop."""
global _interrupted, current_task
_interrupted = True
if current_task and not current_task.done():
current_task.cancel()
raise KeyboardInterrupt
# Set up signal handler that cancels asyncio task
_original_sigint_handler = signal.signal(signal.SIGINT, _handle_interrupt)
try:
while True:
# Check if we were interrupted by Ctrl+C
if _interrupted:
print("\n\n[Ctrl+C detected]")
break
try:
user_input = _get_input("You: ")
except KeyboardInterrupt:
print("\n\n[Ctrl+C detected]")
break
except EOFError:
print("\nInput ended.")
break
if not user_input:
continue
if user_input.lower() == "exit":
break
elif user_input.lower() == "stop":
if current_task and not current_task.done():
current_task.cancel()
print("\n[任务已中断]")
continue
elif user_input.lower() == "save":
# Save without prompting
if memory_file.exists():
print(f"\n对话已保存到: {memory_file}")
else:
print("\n暂无对话可保存")
continue
print() # New line after input
# Run the group chat with the user's message
try:
# Create task and store reference so signal handler can cancel it
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
current_task = loop.create_task(chat.run(task=user_input))
result = loop.run_until_complete(current_task)
finally:
loop.close()
# Reset interrupted flag in case task was cancelled but handled
_interrupted = False
# Print agent responses
for msg in result.messages:
if hasattr(msg, 'source') and msg.source not in ("user", "system"):
content = msg.content if hasattr(msg, 'content') else str(msg)
print(f"\n[{msg.source}]:\n{content}\n")
# Save to memory
save_message_to_memory(memory_file, {
"type": "message",
"sender": msg.source,
"content": content,
"timestamp": datetime.now().isoformat(),
})
has_messages = True
except asyncio.CancelledError:
print("\n[任务已取消]")
_interrupted = False
continue
except Exception as e:
print(f"Error: {e}")
continue
except EOFError:
print("\n\nInput ended.")
finally:
# Restore original signal handler
signal.signal(signal.SIGINT, _original_sigint_handler)
# Always ask if user wants to save (after Ctrl+C or normal exit)
# Default to NO to prevent automatic saves
save = "n"
try:
save = _get_input("\n保存对话? [Y/n]: ").lower()
except (KeyboardInterrupt, EOFError):
print() # newline after Ctrl+C
save = "y"
# Normalize input
if save in ("n", "no"):
try:
memory_file.unlink(missing_ok=True)
print("对话已丢弃")
except Exception as e:
print(f"无法删除文件: {e}")
else:
print(f"\n对话已保存到: {memory_file}")
# Update last_chat
config["groups"][group_name]["last_chat"] = datetime.now().isoformat()
save_groups_config(config)
def save_chat_messages_sync(chat, memory_file: Path) -> None:
"""Save chat messages to memory file (sync version)."""
try:
messages = []
if hasattr(chat, 'messages'):
messages = chat.messages or []
for msg in messages:
if hasattr(msg, 'content'):
content = msg.content
sender = getattr(msg, 'name', 'unknown')
elif isinstance(msg, dict):
content = msg.get('content', '')
sender = msg.get('name', msg.get('role', 'unknown'))
else:
continue
save_message_to_memory(memory_file, {
"type": "message",
"sender": sender,
"content": content,
"timestamp": datetime.now().isoformat(),
})
except Exception as e:
print(f"Warning: Could not save messages to memory: {e}")
# =============================================================================
# CLI Command Handler
# =============================================================================
def group_command(args: Any) -> None:
"""Main entry point for group commands."""
action = getattr(args, "group_action", None)
if action is None:
# No subcommand, show help
print("hermes group - Multi-agent group chat management")
print("\nCommands:")
print(" agents List all registered agents")
print(" register <name> Register an agent from profile")
print(" unregister <name> Unregister an agent")
print(" create <name> <agents> Create a group (space-separated agent names)")
print(" list List all groups")
print(" delete <name> Delete a group")
print(" info <name> Show group details")
print(" add <group> <agent> Add agent to group")
print(" remove <group> <agent> Remove agent from group")
print(" chat <name> Start group chat")
print(" continue <file> Continue from memory file")
print(" memory List memory files")
return
# Agent commands
if action == "agents":
if getattr(args, "register", None):
# hermes group agents register <name>
name = args.register
profile = getattr(args, "profile", None)
if register_agent(name, profile):
print(f"Agent '{name}' registered from profile '{profile or name}'")
elif getattr(args, "unregister", None):
# hermes group agents unregister <name>
name = args.unregister
if unregister_agent(name):
print(f"Agent '{name}' unregistered")
else:
# List agents
agents = list_agents()
if not agents:
print("No agents registered.")
print("Run 'hermes group agents register <name>' to register an agent")
return
print(f"\nRegistered Agents ({len(agents)}):")
print("-" * 40)
for agent in agents:
print(f" {agent['name']}")
print(f" Profile: {agent.get('profile', 'N/A')}")
print(f" Registered: {agent.get('registered_at', 'N/A')}")
print()
elif action == "register":
name = getattr(args, "agent_name", None)
if not name:
print("Error: Agent name required")
print("Usage: hermes group register <name> [--profile <profile>]")
return
profile = getattr(args, "profile", None)
register_agent(name, profile)
elif action == "unregister":
name = getattr(args, "agent_name", None)
if not name:
print("Error: Agent name required")
return
unregister_agent(name)
# Group commands
elif action == "create":
name = getattr(args, "group_name", None)
agents = getattr(args, "agents", [])
if not name:
print("Error: Group name required")
print("Usage: hermes group create <name> <agent1> <agent2> ...")
return
if len(agents) < 2:
print("Error: A group must have at least 2 agents")
print("Usage: hermes group create <name> <agent1> <agent2> ...")
return
create_group(name, agents)
elif action == "list":
groups = list_groups()
if not groups:
print("No groups created.")
print("Run 'hermes group create <name> <agents...>' to create a group")
return
print(f"\nGroups ({len(groups)}):")
print("-" * 50)
for g in groups:
print(f" {g['name']}")
print(f" Agents: {', '.join(g['agents'])}")
print(f" Created: {g['created_at']}")
if g.get('last_chat'):
print(f" Last chat: {g['last_chat']}")
print()
elif action == "delete":
name = getattr(args, "group_name", None)
if not name:
print("Error: Group name required")
return
delete_group(name)
elif action == "info":
name = getattr(args, "group_name", None)
if not name:
print("Error: Group name required")
return
info = show_group_info(name)
if info:
print(f"\nGroup: {info['name']}")
print("-" * 40)
print(f"Agents: {', '.join(info.get('agents', []))}")
print(f"Created: {info.get('created_at', 'N/A')}")
print(f"Last chat: {info.get('last_chat', 'Never')}")
elif action == "add":
group_name = getattr(args, "group_name", None)
agent_name = getattr(args, "agent_name", None)
if not group_name or not agent_name:
print("Error: Group name and agent name required")
return
add_agent_to_group(group_name, agent_name)
elif action == "remove":
group_name = getattr(args, "group_name", None)
agent_name = getattr(args, "agent_name", None)
if not group_name or not agent_name:
print("Error: Group name and agent name required")
return
remove_agent_from_group(group_name, agent_name)
elif action == "chat":
name = getattr(args, "group_name", None)
if not name:
print("Error: Group name required")
print("Usage: hermes group chat <name>")
return
run_group_chat(name)
elif action == "continue":
filename = getattr(args, "memory_file", None)
if not filename:
print("Error: Memory file required")
print("Usage: hermes group continue <memory_file>")
return
path = Path(filename)
if not path.exists():
# Try to find in memory directory
path = MEMORY_DIR / filename
if not path.exists():
print(f"Error: Memory file '{filename}' not found")
return
# Extract group name from filename
# Format: YYYY-MM-DD_HH-MM-SS_groupname.jsonl
group_name = path.stem.split("_", 2)[-1] if "_" in path.stem else path.stem
run_group_chat(group_name, path)
elif action == "memory":
files = list_memory_files()
if not files:
print("No memory files found.")
print("Memory files are created when you run 'hermes group chat'")
return
print(f"\nMemory Files ({len(files)}):")
print("-" * 60)
for f in files:
print(f" {f['name']}")
print(f" Size: {f['size']} bytes")
print(f" Modified: {f['modified']}")
print()

View file

@ -8112,6 +8112,133 @@ Examples:
plugins_parser.set_defaults(func=cmd_plugins)
# =========================================================================
# group command - Multi-agent group chat management
# =========================================================================
group_parser = subparsers.add_parser(
"group",
help="Multi-agent group chat management",
description="Create and manage multi-agent group chats with shared memory",
)
group_subparsers = group_parser.add_subparsers(dest="group_action")
# hermes group agents
group_agents = group_subparsers.add_parser(
"agents",
help="List all registered agents",
description="Show all registered agents from profiles",
)
group_agents.add_argument(
"register",
nargs="?",
help="Register argument (internal)",
)
group_agents.add_argument(
"unregister",
nargs="?",
help="Unregister argument (internal)",
)
group_agents.add_argument(
"--profile",
help="Profile name to register from",
)
# hermes group register <name>
group_register = group_subparsers.add_parser(
"register",
help="Register an agent from a profile",
description="Register an agent using an existing Hermes profile",
)
group_register.add_argument("agent_name", help="Agent name")
group_register.add_argument("--profile", help="Profile name (defaults to agent name)")
# hermes group unregister <name>
group_unregister = group_subparsers.add_parser(
"unregister",
help="Unregister an agent",
description="Remove an agent from the registry",
)
group_unregister.add_argument("agent_name", help="Agent name to unregister")
# hermes group create <name> <agents...>
group_create = group_subparsers.add_parser(
"create",
help="Create a new group",
description="Create a group with specified agents (min 2)",
)
group_create.add_argument("group_name", help="Group name")
group_create.add_argument("agents", nargs="+", help="Agent names (space-separated)")
# hermes group list
group_subparsers.add_parser(
"list",
help="List all groups",
description="Show all created groups",
)
# hermes group delete <name>
group_delete = group_subparsers.add_parser(
"delete",
help="Delete a group",
description="Remove a group by name",
)
group_delete.add_argument("group_name", help="Group name to delete")
# hermes group info <name>
group_info = group_subparsers.add_parser(
"info",
help="Show group details",
description="Display detailed information about a group",
)
group_info.add_argument("group_name", help="Group name")
# hermes group add <group> <agent>
group_add = group_subparsers.add_parser(
"add",
help="Add an agent to a group",
description="Add an existing agent to a group",
)
group_add.add_argument("group_name", help="Group name")
group_add.add_argument("agent_name", help="Agent name to add")
# hermes group remove <group> <agent>
group_remove = group_subparsers.add_parser(
"remove",
help="Remove an agent from a group",
description="Remove an agent from a group",
)
group_remove.add_argument("group_name", help="Group name")
group_remove.add_argument("agent_name", help="Agent name to remove")
# hermes group chat <name>
group_chat = group_subparsers.add_parser(
"chat",
help="Start a group chat",
description="Start an interactive group chat session",
)
group_chat.add_argument("group_name", help="Group name to chat with")
# hermes group continue <file>
group_continue = group_subparsers.add_parser(
"continue",
help="Continue from a memory file",
description="Resume a group chat from a saved memory file",
)
group_continue.add_argument("memory_file", help="Memory filename or path")
# hermes group memory
group_subparsers.add_parser(
"memory",
help="List memory files",
description="Show all saved group chat memory files",
)
def cmd_group(args):
from hermes_cli.group_commands import group_command
group_command(args)
group_parser.set_defaults(func=cmd_group)
# =========================================================================
# Plugin CLI commands — dynamically registered by memory/general plugins.
# Plugins provide a register_cli(subparser) function that builds their