diff --git a/src/shell_history_search/cli/commands.py b/src/shell_history_search/cli/commands.py new file mode 100644 index 0000000..c461d07 --- /dev/null +++ b/src/shell_history_search/cli/commands.py @@ -0,0 +1,109 @@ +import click +import json +import logging +from datetime import datetime + +from ..core import SearchEngine, IndexingService + +logger = logging.getLogger(__name__) + + +@click.group() +@click.option("-v", "--verbose", count=True, help="Increase verbosity") +def cli(verbose: int) -> None: + log_level = logging.WARNING + if verbose == 1: + log_level = logging.INFO + elif verbose >= 2: + log_level = logging.DEBUG + logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s") + + +@cli.command() +@click.option("--shell", type=str, help="Index only specific shell (bash, zsh, fish)") +def index(shell: str | None) -> None: + click.echo("Indexing shell history...") + + indexing_service = IndexingService() + + shell_type = shell.lower() if shell else None + result = indexing_service.index_shell_history(shell_type) + + click.echo( + f"Indexed {result['total_indexed']} commands " + f"(skipped {result['total_skipped']} duplicates)" + ) + + +@cli.command() +@click.argument("query") +@click.option("--limit", "-n", type=int, default=10, help="Number of results to return") +@click.option("--shell", type=str, help="Filter by shell type (bash, zsh, fish)") +@click.option("--json", "json_output", is_flag=True, help="Output as JSON") +def search(query: str, limit: int, shell: str | None, json_output: bool) -> None: + search_engine = SearchEngine() + + shell_type = shell.lower() if shell else None + + results = search_engine.search(query, limit=limit, shell_type=shell_type) + + if not results: + click.echo("No results found.") + return + + if json_output: + output = [ + { + "command": r.command, + "shell": r.shell_type, + "timestamp": r.timestamp, + "similarity": round(r.similarity, 4), + } + for r in results + ] + click.echo(json.dumps(output, indent=2)) + else: + for i, result in enumerate(results, 1): + timestamp_str = "" + if result.timestamp: + dt = datetime.fromtimestamp(result.timestamp) + timestamp_str = dt.strftime("%Y-%m-%d %H:%M") + + similarity_pct = result.similarity * 100 + click.echo(f"{i}. [{result.shell_type}] {timestamp_str}") + click.echo(f" {result.command}") + click.echo(f" Similarity: {similarity_pct:.1f}%") + click.echo() + + +@cli.command() +@click.option("--json", "json_output", is_flag=True, help="Output as JSON") +def stats(json_output: bool) -> None: + search_engine = SearchEngine() + stats_data = search_engine.get_stats() + + if json_output: + click.echo(json.dumps(stats_data, indent=2)) + else: + click.echo("Shell History Search Statistics") + click.echo("=" * 40) + click.echo(f"Total commands: {stats_data['total_commands']}") + click.echo(f"Total embeddings: {stats_data['total_embeddings']}") + click.echo(f"Embedding model: {stats_data['embedding_model']}") + click.echo(f"Embedding dimension: {stats_data['embedding_dim']}") + click.echo() + click.echo("Commands by shell:") + for shell_type, count in stats_data["shell_counts"].items(): + click.echo(f" {shell_type}: {count}") + + +@cli.command() +@click.confirmation_option(prompt="Are you sure you want to clear all indexed data?") +def clear() -> None: + search_engine = SearchEngine() + search_engine.clear_all() + click.echo("All indexed data has been cleared.") + + +if __name__ == "__main__": + cli() \ No newline at end of file