cmdgen-ai-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cmdgen/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ cmdgen - AI-powered CLI tool to translate natural language into terminal commands
3
+ """
4
+
5
+ __version__ = "0.1.0"
cmdgen/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from cmdgen.main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
cmdgen/config.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ cmdgen/config.py
3
+
4
+ Purpose:
5
+ This file is responsible for managing the user's settings. It defines what the
6
+ configuration looks like (using `pydantic`), and handles safely loading and
7
+ saving the API keys to a local file on the user's hard drive (`~/.config/cmdgen/config.json`).
8
+ """
9
+ import os
10
+ import json
11
+ from pathlib import Path
12
+ from pydantic import BaseModel, Field
13
+
14
+ CONFIG_DIR = Path.home() / ".config" / "cmdgen"
15
+ CONFIG_FILE = CONFIG_DIR / "config.json"
16
+
17
+ class AppConfig(BaseModel):
18
+ provider: str = Field(default="gemini", description="The LLM provider to use")
19
+ api_key: str = Field(default="", description="The API key for the provider")
20
+
21
+ def load_config() -> AppConfig:
22
+ """Loads the configuration from disk, or returns default if not found."""
23
+ if not CONFIG_FILE.exists():
24
+ return AppConfig()
25
+ try:
26
+ with open(CONFIG_FILE, "r") as f:
27
+ data = json.load(f)
28
+ return AppConfig(**data)
29
+ except Exception:
30
+ # In case of corruption, return default
31
+ return AppConfig()
32
+
33
+ def save_config(config: AppConfig) -> None:
34
+ """Saves the configuration to disk securely."""
35
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
36
+ with open(CONFIG_FILE, "w") as f:
37
+ json.dump(config.model_dump(), f, indent=4)
38
+
39
+ # Ensure strict permissions on Windows/Linux if possible
40
+ try:
41
+ os.chmod(CONFIG_FILE, 0o600)
42
+ except Exception:
43
+ pass # Permissions might act differently on Windows
cmdgen/context.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ cmdgen/context.py
3
+
4
+ Purpose:
5
+ This file gathers information about the environment where the user is running the tool.
6
+ It detects the Operating System, the shell (e.g. bash, powershell), and the current
7
+ working directory so that the AI can generate commands tailored perfectly to their system.
8
+ """
9
+ import os
10
+ import platform
11
+
12
+ def get_os_name() -> str:
13
+ """Returns the name of the operating system."""
14
+ return platform.system() + " " + platform.release()
15
+
16
+ def get_shell_name() -> str:
17
+ """Attempts to determine the current shell running the tool."""
18
+ # On Windows, COMSPEC is usually set to cmd.exe or powershell.exe
19
+ # On Unix, SHELL is usually set to /bin/bash, /bin/zsh, etc.
20
+ shell = os.environ.get("SHELL")
21
+ if shell:
22
+ return shell.split(os.sep)[-1]
23
+
24
+ comspec = os.environ.get("COMSPEC")
25
+ if comspec:
26
+ return comspec.split(os.sep)[-1]
27
+
28
+ return "unknown"
29
+
30
+ def get_cwd() -> str:
31
+ """Returns the current working directory."""
32
+ return os.getcwd()
cmdgen/executor.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ cmdgen/executor.py
3
+
4
+ Purpose:
5
+ This file handles the interactive prompt and execution of the final command.
6
+ It uses `prompt_toolkit` to allow the user to edit the generated command
7
+ before running it, and `subprocess` to actually run the command on the OS.
8
+ """
9
+ import subprocess
10
+ import sys
11
+ from rich.console import Console
12
+ from prompt_toolkit import PromptSession
13
+ from prompt_toolkit.lexers import PygmentsLexer
14
+ from pygments.lexers.shell import BashLexer
15
+
16
+ console = Console()
17
+
18
+ DANGEROUS_KEYWORDS = [
19
+ "rm ", "del ", "format ", "drop ", "mkfs", "truncate"
20
+ ]
21
+
22
+ def check_safety(command: str) -> None:
23
+ """Checks if a command contains potentially dangerous keywords."""
24
+ cmd_lower = command.lower()
25
+ if any(keyword in cmd_lower for keyword in DANGEROUS_KEYWORDS):
26
+ console.print("[bold yellow][WARNING] This command may be destructive! Please review carefully.[/bold yellow]")
27
+
28
+ def execute_interactive(command: str) -> None:
29
+ """
30
+ Presents the generated command to the user for editing and executes it.
31
+ """
32
+ check_safety(command)
33
+
34
+ session = PromptSession()
35
+ try:
36
+ # Prompt the user, pre-filled with the AI's command
37
+ final_command = session.prompt(
38
+ "> ",
39
+ default=command,
40
+ lexer=PygmentsLexer(BashLexer)
41
+ )
42
+
43
+ final_command = final_command.strip()
44
+ if not final_command:
45
+ console.print("[dim]Command cancelled.[/dim]")
46
+ return
47
+
48
+ # Execute natively
49
+ console.print()
50
+ subprocess.run(final_command, shell=True)
51
+
52
+ except KeyboardInterrupt:
53
+ # User pressed Ctrl+C
54
+ console.print("\n[dim]Command cancelled by user.[/dim]")
55
+ except EOFError:
56
+ # User pressed Ctrl+D
57
+ console.print("\n[dim]Command cancelled by user.[/dim]")
58
+ except Exception as e:
59
+ console.print(f"[bold red]Failed to execute command:[/bold red] {e}")
cmdgen/llm/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ cmdgen LLM providers module.
3
+ """
cmdgen/llm/base.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ cmdgen/llm/base.py
3
+
4
+ Purpose:
5
+ This file defines the `LLMProvider` abstract base class. It serves as a blueprint or
6
+ "contract". It guarantees that no matter what AI provider we add in the future (Gemini,
7
+ OpenAI, Claude, etc.), they will all have a `generate_command()` method with the same inputs.
8
+ """
9
+ from abc import ABC, abstractmethod
10
+ from typing import Dict, Any
11
+
12
+ class LLMProvider(ABC):
13
+ """Abstract base class for all LLM providers."""
14
+
15
+ @abstractmethod
16
+ def generate_command(
17
+ self,
18
+ query: str,
19
+ os_name: str,
20
+ shell_name: str,
21
+ cwd: str,
22
+ api_key: str
23
+ ) -> tuple[str, str]:
24
+ """
25
+ Generate a terminal command based on a natural language query.
26
+
27
+ Args:
28
+ query: The user's natural language request.
29
+ os_name: The operating system (e.g., Windows, Linux).
30
+ shell_name: The shell being used (e.g., powershell, bash).
31
+ cwd: The current working directory.
32
+ api_key: The API key for the provider.
33
+
34
+ Returns:
35
+ A tuple of (generated_command, explanation).
36
+ """
37
+ pass
cmdgen/llm/gemini.py ADDED
@@ -0,0 +1,66 @@
1
+ """
2
+ cmdgen/llm/gemini.py
3
+
4
+ Purpose:
5
+ This file contains the actual implementation for the Google Gemini AI. It takes the
6
+ user's query and their system context, constructs the hidden system prompt (with
7
+ safety filters), and calls the `google-genai` API to get the command back.
8
+ """
9
+ from google import genai
10
+ from google.genai import types
11
+
12
+ from cmdgen.llm.base import LLMProvider
13
+
14
+ class GeminiProvider(LLMProvider):
15
+ """Gemini implementation of the LLM Provider."""
16
+
17
+ def generate_command(
18
+ self,
19
+ query: str,
20
+ os_name: str,
21
+ shell_name: str,
22
+ cwd: str,
23
+ api_key: str
24
+ ) -> tuple[str, str]:
25
+
26
+ # Initialize the new SDK client
27
+ client = genai.Client(api_key=api_key)
28
+
29
+ # Use gemini-2.5-flash, the fast model
30
+ model_name = 'gemini-2.5-flash'
31
+
32
+ system_prompt = (
33
+ "You are an expert systems administrator CLI assistant.\n"
34
+ "Your job is to translate the user's natural language request into a valid, safe terminal command.\n\n"
35
+ f"CONTEXT:\n"
36
+ f"- OS: {os_name}\n"
37
+ f"- Shell: {shell_name}\n"
38
+ f"- Current Working Directory: {cwd}\n\n"
39
+ "RULES:\n"
40
+ "1. Output ONLY the raw command on the first line.\n"
41
+ "2. Output a brief, 1-sentence explanation on the second line.\n"
42
+ "3. Do not use markdown blocks (```) or quotes around the command.\n"
43
+ "4. Ensure paths are compatible with the specified OS."
44
+ )
45
+
46
+ prompt = f"{system_prompt}\n\nUSER REQUEST: {query}\n\nCOMMAND AND EXPLANATION:"
47
+
48
+ try:
49
+ response = client.models.generate_content(
50
+ model=model_name,
51
+ contents=prompt,
52
+ config=types.GenerateContentConfig(
53
+ temperature=0.1 # Low temp for deterministic output
54
+ )
55
+ )
56
+
57
+ text = response.text.strip()
58
+ lines = text.split('\n', 1)
59
+
60
+ command = lines[0].strip()
61
+ explanation = lines[1].strip() if len(lines) > 1 else "No explanation provided."
62
+
63
+ return command, explanation
64
+
65
+ except Exception as e:
66
+ raise RuntimeError(f"Gemini API Error: {str(e)}")
cmdgen/main.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ cmdgen/main.py
3
+
4
+ Purpose:
5
+ This is the primary entry point for the CLI application. It uses the `typer` library
6
+ to define all the commands the user can run (like `cmdgen "query"` and `cmdgen config`).
7
+ It connects the user's input to the configuration, context, and AI logic.
8
+ """
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from cmdgen.config import load_config, save_config
15
+
16
+ app = typer.Typer(help="AI-powered terminal command generator.")
17
+ config_app = typer.Typer(help="Manage configuration (e.g. API keys).")
18
+ app.add_typer(config_app, name="config")
19
+
20
+ console = Console()
21
+
22
+ @app.command()
23
+ def generate(
24
+ query: list[str] = typer.Argument(..., help="The natural language query to translate into a command")
25
+ ):
26
+ """
27
+ Generate and execute a terminal command from natural language.
28
+ """
29
+ query_str = " ".join(query)
30
+ config = load_config()
31
+ if not config.api_key:
32
+ console.print(Panel(
33
+ "[bold red]API Key Missing[/bold red]\n\n"
34
+ "To use cmdgen, you need to set up your free Gemini API key.\n"
35
+ "1. Get your key here: [link]https://aistudio.google.com/app/apikey[/link]\n"
36
+ "2. Set it by running: [bold cyan]cmdgen config set --provider gemini --api-key \"YOUR_KEY\"[/bold cyan]",
37
+ title="Setup Required", expand=False
38
+ ))
39
+ raise typer.Exit(code=1)
40
+
41
+ # Check for new updates silently
42
+ from cmdgen.update import check_for_updates
43
+ check_for_updates()
44
+
45
+ with console.status(f"[bold green]Asking {config.provider}...", spinner="dots"):
46
+ from cmdgen.llm.gemini import GeminiProvider
47
+ from cmdgen.context import get_os_name, get_shell_name, get_cwd
48
+
49
+ provider = GeminiProvider() # We can make this dynamic later if we add OpenAI
50
+ try:
51
+ command, explanation = provider.generate_command(
52
+ query=query_str,
53
+ os_name=get_os_name(),
54
+ shell_name=get_shell_name(),
55
+ cwd=get_cwd(),
56
+ api_key=config.api_key
57
+ )
58
+ except Exception as e:
59
+ console.print(f"[bold red]Error generating command:[/bold red] {e}")
60
+ raise typer.Exit(code=1)
61
+
62
+ console.print(f"\n[bold blue]Explanation:[/bold blue] {explanation}")
63
+ console.print("[dim]---[/dim]")
64
+
65
+ from cmdgen.executor import execute_interactive
66
+ execute_interactive(command)
67
+
68
+ @config_app.command("set")
69
+ def config_set(
70
+ provider: str = typer.Option("gemini", help="The LLM provider (e.g., gemini, openai)"),
71
+ api_key: str = typer.Option(..., help="The API key for the provider")
72
+ ):
73
+ """
74
+ Set the configuration values.
75
+ """
76
+ config = load_config()
77
+ config.provider = provider
78
+ config.api_key = api_key
79
+ save_config(config)
80
+ console.print(f"[green]✔[/green] Configuration saved successfully! Provider set to [bold]{provider}[/bold].")
81
+
82
+ @config_app.command("view")
83
+ def config_view():
84
+ """
85
+ View the current configuration safely.
86
+ """
87
+ config = load_config()
88
+ masked_key = f"{config.api_key[:4]}...{config.api_key[-4:]}" if len(config.api_key) > 8 else "***"
89
+
90
+ console.print(Panel(
91
+ f"[bold]Provider:[/bold] {config.provider}\n"
92
+ f"[bold]API Key:[/bold] {masked_key}",
93
+ title="Current Configuration", expand=False
94
+ ))
95
+
96
+ def cli_main():
97
+ import sys
98
+ # If the first argument is not a known subcommand or flag, default to "generate"
99
+ if len(sys.argv) > 1 and sys.argv[1] not in ["config", "generate", "--help", "-h", "--version"]:
100
+ sys.argv.insert(1, "generate")
101
+ app()
102
+
103
+ if __name__ == "__main__":
104
+ cli_main()
cmdgen/update.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ Purpose: Checks PyPI for a newer version of cmdgen and notifies the user.
3
+ Caches the check to avoid slowing down the CLI on every run.
4
+ """
5
+ import urllib.request
6
+ import json
7
+ import time
8
+ from rich.console import Console
9
+
10
+ from cmdgen.config import CONFIG_DIR
11
+
12
+ console = Console()
13
+
14
+ # We cache the update check in the config directory
15
+ UPDATE_CACHE_FILE = CONFIG_DIR / "update_check.json"
16
+ CACHE_TTL = 86400 # 24 hours in seconds
17
+ PACKAGE_NAME = "cmdgen"
18
+
19
+ # Fallback version for development
20
+ def get_current_version() -> str:
21
+ try:
22
+ from importlib.metadata import version, PackageNotFoundError
23
+ return version(PACKAGE_NAME)
24
+ except (ImportError, Exception):
25
+ return "0.1.0"
26
+
27
+ def check_for_updates() -> None:
28
+ """Silently checks PyPI for updates, at most once per day."""
29
+ now = time.time()
30
+
31
+ # Check cache
32
+ if UPDATE_CACHE_FILE.exists():
33
+ try:
34
+ cache_data = json.loads(UPDATE_CACHE_FILE.read_text(encoding="utf-8"))
35
+ if now - cache_data.get("last_check", 0) < CACHE_TTL:
36
+ return # Skip check, too soon
37
+ except Exception:
38
+ pass # If cache is corrupted, ignore it
39
+
40
+ try:
41
+ # Fetch latest version from PyPI
42
+ req = urllib.request.Request(
43
+ f"https://pypi.org/pypi/{PACKAGE_NAME}/json",
44
+ headers={"User-Agent": f"cmdgen/{get_current_version()}"}
45
+ )
46
+ with urllib.request.urlopen(req, timeout=1.5) as response:
47
+ data = json.loads(response.read().decode("utf-8"))
48
+ latest_version = data["info"]["version"]
49
+
50
+ current = get_current_version()
51
+
52
+ # Simple string comparison (this works well enough for simple semver like 0.1.0 vs 0.2.0)
53
+ # We don't want to add a `packaging` dependency just for this.
54
+ if latest_version and latest_version != current:
55
+ console.print(f"[bold yellow]💡 A new version of cmdgen ({latest_version}) is available! Run `pipx upgrade cmdgen` to update.[/bold yellow]")
56
+
57
+ # Update cache
58
+ UPDATE_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
59
+ UPDATE_CACHE_FILE.write_text(json.dumps({"last_check": now, "latest_version": latest_version}), encoding="utf-8")
60
+
61
+ except Exception:
62
+ # If offline or PyPI is down, silently fail. We don't want to bother the user.
63
+ pass
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdgen-ai-cli
3
+ Version: 0.1.0
4
+ Summary: AI-powered CLI tool to translate natural language into terminal commands
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: google-genai>=0.1.0
7
+ Requires-Dist: prompt-toolkit>=3.0.0
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: typer>=0.9.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: black>=23.0; extra == 'dev'
13
+ Requires-Dist: flake8>=6.0; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # cmdgen 🪄
18
+
19
+ An AI-powered CLI tool that translates your natural language into native terminal commands and lets you review them before execution. Powered by Google's Gemini API.
20
+
21
+ ## Features
22
+
23
+ - **Natural Language to CLI:** Just type what you want to do.
24
+ - **Context-Aware:** Knows your OS (Windows/Linux/macOS), Shell, and Working Directory for perfect commands.
25
+ - **Interactive Execution:** Uses `prompt_toolkit` to let you tweak the AI's generated command before running it.
26
+ - **Safety First:** Warns you with bold yellow text if the command looks destructive (e.g., `rm`, `del`, `format`).
27
+ - **Auto-Updater:** Silently checks for updates so you're always running the best version.
28
+
29
+ ## Installation
30
+
31
+ The recommended way to install Python CLI tools globally is using `pipx`:
32
+
33
+ ```bash
34
+ pipx install cmdgen
35
+ ```
36
+
37
+ *(Alternatively, you can `pip install cmdgen` in a virtual environment)*
38
+
39
+ ## Quick Start
40
+
41
+ 1. **Get an API Key:** Get a free Gemini API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
42
+ 2. **Configure your Key:**
43
+ ```bash
44
+ cmdgen config set --provider gemini --api-key "YOUR_API_KEY"
45
+ ```
46
+ 3. **Use the Tool:**
47
+ ```bash
48
+ cmdgen "find all python files larger than 10MB in the current directory"
49
+ ```
50
+
51
+ You will see an explanation of the command, and an interactive prompt where you can edit it or just press `Enter` to run it!
52
+
53
+ ## Configuration
54
+
55
+ View your current configuration (safely masked):
56
+ ```bash
57
+ cmdgen config view
58
+ ```
59
+
60
+ ## Contributing
61
+ 1. Clone the repository
62
+ 2. Install dependencies: `pip install -e .[dev]`
63
+ 3. Run tests: `pytest`
64
+
65
+ ## License
66
+ MIT
@@ -0,0 +1,14 @@
1
+ cmdgen/__init__.py,sha256=wxV1u5iTty9coWhQsiy0VJwtoTDw8WIT-sFG_NZEq4U,113
2
+ cmdgen/__main__.py,sha256=P1upHqRqbajqAI8HNwvOfMmB1jB3Mw3yUYdExBqWvjc,66
3
+ cmdgen/config.py,sha256=HbREfGwP1ucmBFuGGlQtIa2F52W_noN8WJVM-_mIJ7E,1475
4
+ cmdgen/context.py,sha256=Vth2O-HBVWENt3nexJ5I7a3AsIP6hQEQBQ66-7_jvfo,1010
5
+ cmdgen/executor.py,sha256=5Yyxgas1DHgwkQE5aDBrLvIF2efjIGSU2EJe51eTQkc,1926
6
+ cmdgen/main.py,sha256=k1on3afYzdhlaGitd4voYTKHG3cT9CTfonB6efjRqxw,3674
7
+ cmdgen/update.py,sha256=PXN1OTpgL-7v6O5gHSDoY35EPd6jlb9TmyKiSBozTns,2374
8
+ cmdgen/llm/__init__.py,sha256=R-z_OJVCsRBWzQf3PNgM677ZzX-RtA0ikM-t8WgrdYY,37
9
+ cmdgen/llm/base.py,sha256=gm1QGmLqUt1zw7WUgSBmRFmeX3q663sOuR9XVsxAom8,1157
10
+ cmdgen/llm/gemini.py,sha256=aIyIBQoav_-48-ZPkldeV1ytiv8QLHVWndY6Ta01kpQ,2290
11
+ cmdgen_ai_cli-0.1.0.dist-info/METADATA,sha256=0y1JIjejXKrApfnAi27jJNlhSniveM7BV0VWsr9tEF4,2106
12
+ cmdgen_ai_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
13
+ cmdgen_ai_cli-0.1.0.dist-info/entry_points.txt,sha256=Rv5gneL6LCvI_D9mX5oAM_1UCtpl2X-PIfiyUbtkLg0,74
14
+ cmdgen_ai_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cg = cmdgen.main:cli_main
3
+ cmdgen = cmdgen.main:cli_main