From b6343f3c5610b60cbb15aae9376cb779101c114c Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sat, 31 Jan 2026 05:31:19 +0000 Subject: [PATCH] Initial upload: shell-speak CLI tool with natural language to shell command conversion --- shell_speak/main.py | 218 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 shell_speak/main.py diff --git a/shell_speak/main.py b/shell_speak/main.py new file mode 100644 index 0000000..8596366 --- /dev/null +++ b/shell_speak/main.py @@ -0,0 +1,218 @@ +"""Main CLI entry point for shell-speak.""" + +import os +import sys +from typing import Optional + +import typer +from rich.panel import Panel +from rich.text import Text + +from shell_speak import __version__ +from shell_speak.config import DEFAULT_TOOLS, ensure_data_dir, get_history_file +from shell_speak.history import get_history_manager +from shell_speak.interactive import run_interactive_mode +from shell_speak.library import get_loader +from shell_speak.matcher import get_matcher +from shell_speak.output import ( + console, + display_command, + display_error, + display_history, + display_info, +) +from shell_speak.models import CommandMatch + +app = typer.Typer( + name="shell-speak", + add_completion=False, + help="Convert natural language to shell commands", +) + + +def version_callback(value: bool): + """Show version information.""" + if value: + console.print(f"Shell Speak v{__version__}") + raise typer.Exit() + + +@app.callback() +def main( + version: bool = typer.Option( + False, + "--version", + "-V", + callback=version_callback, + is_eager=True, + help="Show version information", + ), +): + pass + + +@app.command() +def convert( + query: str = typer.Argument(..., help="Natural language description of the command"), + tool: Optional[str] = typer.Option( + None, + "--tool", + "-t", + help=f"Filter by tool: {', '.join(DEFAULT_TOOLS)}", + ), + explain: bool = typer.Option( + False, + "--explain", + "-e", + help="Show detailed explanation of the command", + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + "-n", + help="Preview the command without executing", + ), +): + """Convert natural language to a shell command.""" + ensure_data_dir() + + matcher = get_matcher() + match = matcher.match(query, tool) + + if match: + display_command(match, explain=explain) + + if dry_run: + display_info("Dry run - command not executed") + else: + display_info("Use --dry-run to preview without execution") + else: + display_error(f"Could not find a matching command for: '{query}'") + display_info("Try using --tool to specify which tool you're using") + + +@app.command() +def interactive( + interactive_mode: bool = typer.Option( + False, + "--interactive", + "-i", + is_eager=True, + help="Enter interactive mode", + ), +): + """Enter interactive mode with history and auto-completion.""" + run_interactive_mode() + + +@app.command() +def history( + limit: int = typer.Option( + 20, + "--limit", + "-l", + help="Number of entries to show", + ), + tool: Optional[str] = typer.Option( + None, + "--tool", + "-t", + help=f"Filter by tool: {', '.join(DEFAULT_TOOLS)}", + ), + search: Optional[str] = typer.Option( + None, + "--search", + "-s", + help="Search history for query", + ), +): + """View command history.""" + ensure_data_dir() + history_manager = get_history_manager() + history_manager.load() + + if search: + entries = history_manager.search(search, tool) + else: + entries = history_manager.get_recent(limit) + + if entries: + display_history(entries, limit) + else: + display_info("No history entries found") + + +@app.command() +def learn( + query: str = typer.Argument(..., help="The natural language query"), + command: str = typer.Argument(..., help="The shell command to associate"), + tool: str = typer.Option( + "custom", + "--tool", + "-t", + help=f"Tool category: {', '.join(DEFAULT_TOOLS)}", + ), +): + """Learn a new command pattern from your correction.""" + ensure_data_dir() + loader = get_loader() + loader.load_library() + loader.add_correction(query, command, tool) + display_info(f"Learned: '{query}' -> '{command}'") + + +@app.command() +def forget( + query: str = typer.Argument(..., help="The query to forget"), + tool: str = typer.Option( + "custom", + "--tool", + "-t", + help="Tool category", + ), +): + """Forget a learned pattern.""" + ensure_data_dir() + loader = get_loader() + loader.load_library() + + if loader.remove_correction(query, tool): + display_info(f"Forgot pattern for: '{query}'") + else: + display_error(f"Pattern not found: '{query}'") + + +@app.command() +def reload(): + """Reload command libraries and corrections.""" + ensure_data_dir() + loader = get_loader() + loader.reload() + display_info("Command libraries reloaded") + + +@app.command() +def tools(): + """List available tools.""" + console.print(Panel( + Text("Available Tools", justify="center", style="bold cyan"), + expand=False, + )) + for tool in DEFAULT_TOOLS: + console.print(f" [tool]{tool}[/]") + + +def main_entry(): + """Entry point for the CLI.""" + try: + app() + except KeyboardInterrupt: + console.print("\n[info]Interrupted.[/]") + sys.exit(130) + except Exception as e: + display_error(f"An error occurred: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main_entry()