add session resumption for cli with easy copy paste command

This commit is contained in:
teknium1
2026-02-25 22:56:12 -08:00
parent e5bd25c73f
commit d2c932d3ac
2 changed files with 96 additions and 6 deletions

78
cli.py
View File

@@ -744,6 +744,7 @@ class HermesCLI:
max_turns: int = 60,
verbose: bool = False,
compact: bool = False,
resume: str = None,
):
"""
Initialize the Hermes CLI.
@@ -757,6 +758,7 @@ class HermesCLI:
max_turns: Maximum tool-calling iterations (default: 60)
verbose: Enable verbose logging
compact: Use compact display mode
resume: Session ID to resume (restores conversation history from SQLite)
"""
# Initialize Rich console
self.console = Console()
@@ -832,12 +834,16 @@ class HermesCLI:
# Conversation state
self.conversation_history: List[Dict[str, Any]] = []
self.session_start = datetime.now()
self._resumed = False
# Generate session ID with timestamp for display and logging
# Format: YYYYMMDD_HHMMSS_shortUUID (e.g., 20260201_143052_a1b2c3)
timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S")
short_uuid = uuid.uuid4().hex[:6]
self.session_id = f"{timestamp_str}_{short_uuid}"
# Session ID: reuse existing one when resuming, otherwise generate fresh
if resume:
self.session_id = resume
self._resumed = True
else:
timestamp_str = self.session_start.strftime("%Y%m%d_%H%M%S")
short_uuid = uuid.uuid4().hex[:6]
self.session_id = f"{timestamp_str}_{short_uuid}"
# History file for persistent input recall across sessions
self._history_file = Path.home() / ".hermes_history"
@@ -890,6 +896,7 @@ class HermesCLI:
def _init_agent(self) -> bool:
"""
Initialize the agent on first use.
When resuming a session, restores conversation history from SQLite.
Returns:
bool: True if successful, False otherwise
@@ -908,6 +915,34 @@ class HermesCLI:
except Exception as e:
logger.debug("SQLite session store not available: %s", e)
# If resuming, validate the session exists and load its history
if self._resumed and self._session_db:
session_meta = self._session_db.get_session(self.session_id)
if not session_meta:
_cprint(f"\033[1;31mSession not found: {self.session_id}{_RST}")
_cprint(f"{_DIM}Use a session ID from a previous CLI run (hermes sessions list).{_RST}")
return False
restored = self._session_db.get_messages_as_conversation(self.session_id)
if restored:
self.conversation_history = restored
msg_count = len([m for m in restored if m.get("role") == "user"])
_cprint(
f"{_GOLD}↻ Resumed session {_BOLD}{self.session_id}{_RST}{_GOLD} "
f"({msg_count} user message{'s' if msg_count != 1 else ''}, "
f"{len(restored)} total messages){_RST}"
)
else:
_cprint(f"{_GOLD}Session {self.session_id} found but has no messages. Starting fresh.{_RST}")
# Re-open the session (clear ended_at so it's active again)
try:
self._session_db._conn.execute(
"UPDATE sessions SET ended_at = NULL, end_reason = NULL WHERE id = ?",
(self.session_id,),
)
self._session_db._conn.commit()
except Exception:
pass
try:
self.agent = AIAgent(
model=self.model,
@@ -1903,6 +1938,32 @@ class HermesCLI:
print(f"Error: {e}")
return None
def _print_exit_summary(self):
"""Print session resume info on exit, similar to Claude Code."""
print()
msg_count = len(self.conversation_history)
if msg_count > 0:
user_msgs = len([m for m in self.conversation_history if m.get("role") == "user"])
tool_calls = len([m for m in self.conversation_history if m.get("role") == "tool" or m.get("tool_calls")])
elapsed = datetime.now() - self.session_start
hours, remainder = divmod(int(elapsed.total_seconds()), 3600)
minutes, seconds = divmod(remainder, 60)
if hours > 0:
duration_str = f"{hours}h {minutes}m {seconds}s"
elif minutes > 0:
duration_str = f"{minutes}m {seconds}s"
else:
duration_str = f"{seconds}s"
print(f"Resume this session with:")
print(f" hermes --resume {self.session_id}")
print()
print(f"Session: {self.session_id}")
print(f"Duration: {duration_str}")
print(f"Messages: {msg_count} ({user_msgs} user, {tool_calls} tool calls)")
else:
print("Goodbye! ⚕")
def run(self):
"""Run the interactive CLI loop with persistent input at bottom."""
self.show_banner()
@@ -2563,7 +2624,7 @@ class HermesCLI:
except Exception as e:
logger.debug("Could not close session in DB: %s", e)
_run_cleanup()
print("\nGoodbye! ⚕")
self._print_exit_summary()
# ============================================================================
@@ -2584,6 +2645,7 @@ def main(
list_tools: bool = False,
list_toolsets: bool = False,
gateway: bool = False,
resume: str = None,
):
"""
Hermes Agent CLI - Interactive AI Assistant
@@ -2601,12 +2663,14 @@ def main(
compact: Use compact display mode
list_tools: List available tools and exit
list_toolsets: List available toolsets and exit
resume: Resume a previous session by its ID (e.g., 20260225_143052_a1b2c3)
Examples:
python cli.py # Start interactive mode
python cli.py --toolsets web,terminal # Use specific toolsets
python cli.py -q "What is Python?" # Single query mode
python cli.py --list-tools # List tools and exit
python cli.py --resume 20260225_143052_a1b2c3 # Resume session
"""
# Signal to terminal_tool that we're in interactive mode
# This enables interactive sudo password prompts with timeout
@@ -2655,6 +2719,7 @@ def main(
max_turns=max_turns,
verbose=verbose,
compact=compact,
resume=resume,
)
# Handle list commands (don't init agent for these)
@@ -2676,6 +2741,7 @@ def main(
cli.show_banner()
cli.console.print(f"[bold blue]Query:[/] {query}")
cli.chat(query)
cli._print_exit_summary()
return
# Run interactive mode

View File

@@ -120,6 +120,7 @@ def cmd_chat(args):
"toolsets": args.toolsets,
"verbose": args.verbose,
"query": args.query,
"resume": getattr(args, "resume", None),
}
# Filter out None values
kwargs = {k: v for k, v in kwargs.items() if v is not None}
@@ -763,6 +764,12 @@ For more help on a command:
action="store_true",
help="Show version and exit"
)
parser.add_argument(
"--resume", "-r",
metavar="SESSION_ID",
default=None,
help="Resume a previous session by ID (shortcut for: hermes chat --resume ID)"
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
@@ -797,6 +804,11 @@ For more help on a command:
action="store_true",
help="Verbose output"
)
chat_parser.add_argument(
"--resume", "-r",
metavar="SESSION_ID",
help="Resume a previous session by ID (shown on exit)"
)
chat_parser.set_defaults(func=cmd_chat)
# =========================================================================
@@ -1303,6 +1315,17 @@ For more help on a command:
cmd_version(args)
return
# Handle top-level --resume as shortcut to chat --resume
if args.resume and args.command is None:
args.command = "chat"
args.query = None
args.model = None
args.provider = None
args.toolsets = None
args.verbose = False
cmd_chat(args)
return
# Default to chat if no command specified
if args.command is None:
args.query = None
@@ -1310,6 +1333,7 @@ For more help on a command:
args.provider = None
args.toolsets = None
args.verbose = False
args.resume = None
cmd_chat(args)
return