"""Main CLI entry point for Shell Memory CLI.""" import sys import os from typing import Optional import click from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.text import Text from .database import Database from .commands import CommandLibrary from .patterns import PatternDetector from .sessions import SessionRecorder from .scripts import ScriptGenerator console = Console() pass_db = click.make_pass_decorator(Database) def get_db(db_path: Optional[str] = None): if db_path is None: db_path = os.environ.get("SHELL_MEMORY_DB") return Database(db_path) def get_library(db): return CommandLibrary(db) def get_detector(db): return PatternDetector(db) def get_recorder(db): return SessionRecorder(db) def get_generator(db): return ScriptGenerator(db) def init_db_from_env(): db_path = os.environ.get("SHELL_MEMORY_DB") if db_path: return get_db(db_path) return None @click.group() @click.option("--db", type=click.Path(), help="Path to database file") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.pass_context def main(ctx, db, verbose): ctx.ensure_object(dict) ctx.obj["verbose"] = verbose if db: ctx.obj["db"] = get_db(db) else: ctx.obj["db"] = get_db() @main.group() def cmd(): """Manage your command library.""" pass @cmd.command("add") @click.argument("command") @click.option("--description", "-d", help="Description of the command") @click.option("--tag", "-t", multiple=True, help="Tags for the command") @click.pass_obj def cmd_add(obj, command, description, tag): db = obj.get("db") or get_db() library = get_library(db) cmd_id = library.add(command, description, list(tag)) console.print(f"[green]✓[/green] Command saved with ID: {cmd_id}") @cmd.command("list") @click.option("--limit", "-l", default=20, help="Maximum number of commands to show") @click.pass_obj def cmd_list(obj, limit): db = obj.get("db") or get_db() library = get_library(db) commands = library.list(limit) if not commands: console.print("[yellow]No commands found. Add some with 'shell-memory cmd add'[/yellow]") return table = Table(title="Your Command Library") table.add_column("ID", justify="right", style="cyan") table.add_column("Command", style="green") table.add_column("Description", style="white") table.add_column("Tags", style="magenta") table.add_column("Uses", justify="right", style="yellow") for cmd in commands: tags = ", ".join(cmd.tags) if cmd.tags else "-" table.add_row( str(cmd.id), cmd.command[:50] + ("..." if len(cmd.command) > 50 else ""), cmd.description[:30] + ("..." if len(cmd.description) > 30 else "") if cmd.description else "-", tags, str(cmd.usage_count), ) console.print(table) @cmd.command("search") @click.argument("query") @click.option("--limit", "-l", default=10, help="Maximum number of results") @click.pass_obj def cmd_search(obj, query, limit): db = obj.get("db") or get_db() library = get_library(db) commands = library.search(query, limit) if not commands: console.print(f"[yellow]No commands found for '{query}'[/yellow]") return console.print(f"[bold]Results for '{query}':[/bold]\n") for cmd in commands: tags = ", ".join(cmd.tags) if cmd.tags else "" console.print(f"[cyan]#{cmd.id}[/cyan] {cmd.command}") if cmd.description: console.print(f" [dim]{cmd.description}[/dim]") if tags: console.print(f" [magenta]Tags: {tags}[/magenta]") console.print() @cmd.command("delete") @click.argument("command_id", type=int) @click.pass_obj def cmd_delete(obj, command_id): db = obj.get("db") or get_db() library = get_library(db) if library.delete(command_id): console.print(f"[green]✓[/green] Command #{command_id} deleted") else: console.print(f"[red]✗[/red] Command #{command_id} not found") @cmd.command("similar") @click.argument("command") @click.pass_obj def cmd_similar(obj, command): db = obj.get("db") or get_db() library = get_library(db) similar = library.find_similar(command) if not similar: console.print("[yellow]No similar commands found[/yellow]") return console.print(f"[bold]Similar commands:[/bold]\n") for cmd, score in similar: console.print(f"[cyan]#{cmd.id}[/cyan] [{score}%] {cmd.command}") @main.group() def pattern(): """Detect and manage command patterns.""" pass @pattern.command("detect") @click.option("--min-freq", "-m", default=2, help="Minimum frequency to detect") @click.option("--limit", "-l", default=50, help="Number of recent commands to analyze") @click.pass_obj def pattern_detect(obj, min_freq, limit): db = obj.get("db") or get_db() detector = get_detector(db) patterns = detector.analyze_recent_commands(limit) if not patterns: console.print("[yellow]No patterns detected yet. Use 'shell-memory cmd add' to record more commands.[/yellow]") return table = Table(title="Detected Patterns") table.add_column("ID", justify="right", style="cyan") table.add_column("Pattern", style="green") table.add_column("Frequency", justify="right", style="yellow") for pattern in patterns: pattern_str = " → ".join(map(str, pattern.command_ids)) table.add_row(str(pattern.id), pattern_str, str(pattern.frequency)) console.print(table) @pattern.command("suggestions") @click.pass_obj def pattern_suggestions(obj): db = obj.get("db") or get_db() detector = get_detector(db) shortcuts = detector.suggest_shortcuts() if not shortcuts: console.print("[yellow]No workflow patterns detected yet.[/yellow]") return console.print(Panel(Text("Suggested Shortcuts", style="bold cyan"), expand=False)) for pattern, commands in shortcuts: console.print(f"\n[bold]Pattern (seen {pattern.frequency}x):[/bold]") for cmd in commands: console.print(f" → {cmd.command}") console.print() @pattern.command("stats") @click.option("--days", "-d", default=7, help="Number of days to analyze") @click.pass_obj def pattern_stats(obj, days): db = obj.get("db") or get_db() detector = get_detector(db) stats = detector.get_workflow_stats(days) console.print(Panel( f"Total Commands: {stats['total_commands']}\n" f"Recent Commands: {stats['recent_commands']}\n" f"Unique Commands: {stats['unique_commands']}\n" f"Patterns Found: {stats['patterns_found']}", title="Workflow Statistics", )) @main.group() def session(): """Record and replay terminal sessions.""" pass @session.command("start") @click.argument("name", required=False) @click.pass_obj def session_start(obj, name): db = obj.get("db") or get_db() recorder = get_recorder(db) session = recorder.start_session(name) console.print(f"[green]✓[/green] Session started: [bold]{session.name}[/bold]") console.print("Recording commands... Use 'shell-memory session stop' to end.") @session.command("record") @click.argument("command") @click.pass_obj def session_record(obj, command): db = obj.get("db") or get_db() recorder = get_recorder(db) if recorder.current_session is None: console.print("[yellow]No active session. Start one with 'shell-memory session start'[/yellow]") return import subprocess try: proc = subprocess.run(command, shell=True, capture_output=True, text=True) output = proc.stdout + proc.stderr success = proc.returncode == 0 recorder.record_command(command, output, success) status = "✓" if success else "✗" console.print(f"[{status}] {command}") except Exception as e: recorder.record_command(command, str(e), False) console.print(f"[✗] {command}") @session.command("stop") @click.pass_obj def session_stop(obj): db = obj.get("db") or get_db() recorder = get_recorder(db) session = recorder.stop_session() if session: console.print(f"[green]✓[/green] Session saved: [bold]{session.name}[/bold]") console.print(f" Commands recorded: {len(session.commands)}") else: console.print("[yellow]No active session to stop[/yellow]") @session.command("list") @click.pass_obj def session_list(obj): db = obj.get("db") or get_db() recorder = get_recorder(db) sessions = recorder.get_sessions() if not sessions: console.print("[yellow]No recorded sessions[/yellow]") return table = Table(title="Recorded Sessions") table.add_column("ID", justify="right", style="cyan") table.add_column("Name", style="green") table.add_column("Commands", justify="right", style="yellow") table.add_column("Date", style="white") for session in sessions: table.add_row( str(session.id), session.name, str(len(session.commands)), session.start_time.strftime("%Y-%m-%d %H:%M"), ) console.print(table) @session.command("replay") @click.argument("session_id", type=int) @click.option("--dry-run", is_flag=True, help="Show commands without executing") @click.option("--delay", "-d", default=0.5, help="Delay between commands (seconds)") @click.pass_obj def session_replay(obj, session_id, dry_run, delay): db = obj.get("db") or get_db() recorder = get_recorder(db) results = recorder.replay_session(session_id, dry_run, delay) if not results: console.print("[red]Session not found[/red]") return if dry_run: console.print("[yellow]Dry run - commands would execute:[/yellow]\n") for result in results: console.print(f" {result['command']}") else: console.print("[bold]Replay results:[/bold]\n") for result in results: status = "✓" if result["success"] else "✗" console.print(f"[{status}] {result['command'][:50]}") @session.command("export") @click.argument("session_id", type=int) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.pass_obj def session_export(obj, session_id, output): db = obj.get("db") or get_db() recorder = get_recorder(db) script = recorder.export_session(session_id) if not script: console.print("[red]Session not found[/red]") return if output: with open(output, "w") as f: f.write(script) console.print(f"[green]✓[/green] Script exported to: {output}") else: console.print(script) @main.group() def script(): """Generate shell scripts from natural language.""" pass @script.command("generate") @click.argument("description") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.pass_obj def script_generate(obj, description, output): db = obj.get("db") or get_db() generator = get_generator(db) result = generator.generate(description) confidence_pct = int(result.confidence * 100) confidence_color = "green" if confidence_pct > 70 else "yellow" if confidence_pct > 40 else "red" console.print(f"[{confidence_color}]Confidence: {confidence_pct}%[/]") if result.matched_keywords: console.print(f"Matched: {', '.join(result.matched_keywords)}") console.print("\n[bold]Generated Script:[/bold]\n") console.print(Panel(result.script, title="Shell Script")) if output: with open(output, "w") as f: f.write(result.script) f.write("\n") console.print(f"\n[green]✓[/green] Script saved to: {output}") @script.command("templates") @click.pass_obj def script_templates(obj): db = obj.get("db") or get_db() generator = get_generator(db) templates = generator.list_templates() if not templates: console.print("[yellow]No templates found[/yellow]") return table = Table(title="Script Templates") table.add_column("Keywords", style="cyan") table.add_column("Description", style="green") table.add_column("Template", style="white") for template in templates: table.add_row( ", ".join(template.keywords), template.description or "-", template.template[:60] + "...", ) console.print(table) @script.command("add-template") @click.argument("keywords", nargs=-1) @click.argument("description") @click.argument("template") @click.pass_obj def script_add_template(obj, keywords, description, template): db = obj.get("db") or get_db() generator = get_generator(db) template_id = generator.add_template(list(keywords), template, description) console.print(f"[green]✓[/green] Template saved with ID: {template_id}") @main.command("version") def version(): """Show version information.""" from . import __version__ console.print(f"Shell Memory CLI v{__version__}") if __name__ == "__main__": main()