From fbdd09509209d28642d0b52ab9987a53d1d4eac9 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Fri, 30 Jan 2026 11:56:13 +0000 Subject: [PATCH] Initial commit: Add shell-memory-cli project A CLI tool that learns from terminal command patterns to automate repetitive workflows. Features: - Command recording with tags and descriptions - Pattern detection for command sequences - Session recording and replay - Natural language script generation --- shell_memory/cli.py | 444 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 shell_memory/cli.py diff --git a/shell_memory/cli.py b/shell_memory/cli.py new file mode 100644 index 0000000..1654a01 --- /dev/null +++ b/shell_memory/cli.py @@ -0,0 +1,444 @@ +"""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() \ No newline at end of file