DEV Community

Cover image for Building My First AI CLI Tool with Python and Gemini
Victor Adejuwon
Victor Adejuwon

Posted on • Originally published at victoradejuwon.substack.com on

Building My First AI CLI Tool with Python and Gemini

A few days ago, I decided to stop reading about AI APIs and integrate one myself.

I wanted a project that would force me to write code, make engineering decisions, and learn by building. After exploring a few ideas, I settled on something simple: a command-line tool that takes plain English and generates Linux commands using an LLM.

That project became devops-ai , a Python CLI tool powered by Google’s Gemini API. It took me about three days to build and taught me a lot about API integration, prompt design, CLI development, and error handling.

The Problem

I spend a lot of time in the terminal, and I regularly find myself searching for commands I’ve forgotten. Sometimes it’s the syntax. Sometimes it’s a flag. Sometimes I know exactly what I want to do but can’t remember the command that gets me there.

I started wondering whether I could stay inside the terminal and describe what I wanted instead.

For example:

python3 devops_ai.py "find all files larger than 100mb"

Enter fullscreen mode Exit fullscreen mode

returns:

find / -type f -size +100M

Enter fullscreen mode Exit fullscreen mode

That became the core idea behind the project.



Building the First Version

The first step was choosing an API provider. I went with Gemini because Google offers a free tier through AI Studio, which made it easy to experiment without worrying about usage costs.

The initial setup was straightforward:

  1. Create a virtual environment

  2. Install dependencies

  3. Generate a Gemini API key

  4. Store the key in a .env file

  5. Connect the application to Gemini

The first version of the script simply accepted a prompt, sent it to Gemini, and printed the response. My goal wasn’t to build a polished application immediately. I wanted proof that the idea worked before investing time in improving the structure.

One of the first issues I ran into was output quality. Gemini often returned explanations, markdown formatting, and additional context. That works well in chat applications, but it isn’t ideal when you’re expecting a shell command.

I solved this by introducing a system prompt that instructed Gemini to behave like a Linux and DevOps expert and return only raw commands. That change immediately improved the responses and gave me my first practical lesson in prompt engineering.


import os
from dotenv import load_dotenv
import google.generativeai as genai

# Load the API key from .env file
load_dotenv()
api_key = os.getenv("GEMINI_API_KEY")

if not api_key:
    print("Error: GEMINI_API_KEY not found in .env file")
    exit(1)

# Configure the Gemini client
genai.configure(api_key=api_key)
model = genai.GenerativeModel("gemini-1.5-flash")

# The system context — tells the AI what role it's playing
SYSTEM_PROMPT = """
You are a Linux and DevOps expert assistant.
When the user describes what they want to do, respond with ONLY the shell command that achieves it.
No explanation, no markdown, no backticks. Just the raw command.
Example:
User: show disk usage sorted by size
Response: du -sh * | sort -rh
"""

def ask_gemini(user_input):
    prompt = f"{SYSTEM_PROMPT}\n\nUser: {user_input}"
    response = model.generate_content(prompt)
    return response.text.strip()

# Main entry point
if __name__ == " __main__":
    user_input = input("What do you want to do? ")
    result = ask_gemini(user_input)
    print(f"\nCommand: {result}\n")
Enter fullscreen mode Exit fullscreen mode

Turning It Into a Real CLI Tool

The project started feeling more like a proper tool when I replaced the basic input flow with Python’s argparse library.

I also added a second feature: command explanation.

Instead of only generating commands, the tool can now explain existing commands:

python3 devops_ai.py --explain "netstat -tulpn"

Enter fullscreen mode Exit fullscreen mode

To support both workflows, I reorganized the application around three functions:

  • ask_gemini() — handles API communication

  • get_command() — generates commands from natural language

  • explain_command() — explains shell commands

Splitting responsibilities made the code easier to understand and easier to extend as the project grew.


import os
import argparse
from dotenv import load_dotenv
from google import genai

# Load the API key from .env file
load_dotenv()
api_key = os.getenv("GEMINI_API_KEY")

if not api_key:
    print("Error: GEMINI_API_KEY not found in .env file")
    exit(1)

# Configure the Gemini client (new SDK style)
client = genai.Client(api_key=api_key)

# Prompt for generating commands
COMMAND_PROMPT = """
You are a Linux and DevOps expert assistant.
When the user describes what they want to do, respond with ONLY the shell command that achieves it.
No explanation, no markdown, no backticks. Just the raw command.
Example:
User: show disk usage sorted by size
Response: du -sh * | sort -rh
"""

# Prompt for explaining commands
EXPLAIN_PROMPT = """
You are a Linux and DevOps expert assistant.
When the user provides a shell command, explain clearly what it does in 2-4 sentences.
Break down each part of the command so a junior engineer can understand it.
No markdown formatting, just plain text.
"""

def ask_gemini(prompt):
    response = client.models.generate_content(
        model=CONFIG['model'],
        contents=prompt
    )
    return response.text.strip()

def get_command(user_input):
    prompt = f"{COMMAND_PROMPT}\n\nUser: {user_input}"
    return ask_gemini(prompt)

def explain_command(command):
    prompt = f"{EXPLAIN_PROMPT}\n\nCommand: {command}"
    return ask_gemini(prompt)

def main():
    # Set up the CLI argument parser
    parser = argparse.ArgumentParser(
        prog="devops-ai",
        description="AI-powered CLI tool for Linux and DevOps commands"
    )

    # Positional argument — the user's query
    parser.add_argument(
        "query",
        type=str,
        help="Describe what you want to do e.g. 'list all running docker containers'"
    )

    # Optional flag — explain mode
    parser.add_argument(
        "--explain",
        action="store_true",
        help="Explain what a command does instead of generating one"
    )

    args = parser.parse_args()

    if args.explain:
        print(f"\nExplaining: {args.query}\n")
        explanation = explain_command(args.query)
        print(f"{explanation}\n")
    else:
        print(f"\nGenerating command for: {args.query}\n")
        command = get_command(args.query)
        print(f"Command: {command}\n")

if __name__ == " __main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Handling Failures

Once the happy path worked, I started thinking about failure scenarios.

I wanted the tool to handle common problems gracefully, so I added:

  • API key validation

  • Retry logic

  • Timeout handling

  • Empty-response checks

  • API-specific error messages

I also created a config.py file to centralize settings such as the model name, retry count, and timeout values.

To verify everything worked, I deliberately broke the application by renaming the .env file and triggering API failures. Those tests helped me improve the messages users see when something goes wrong.

Examples include:

Empty response received from API


API Error: Check your key and try again

Enter fullscreen mode Exit fullscreen mode

def ask_gemini(prompt):
    retries = 0
    while retries < CONFIG['max_retries']:
        try:
            response = client.models.generate_content(
                model = CONFIG['model'],
                contents = prompt
            )

            if not response.text or not response.text.strip():
                print('Received empty response from model. Retrying ...')
                retries+=1
                time.sleep(2)
                continue
            return response.text.strip()

        except genai_errors.APIError as e:
            print(f'API error: {e}')
            print('There is an issue with your API key or request. Please check your API key.\n')
            sys.exit(1)

        except Exception as e:
            retries+=1
            print(f'Unexpected error (attempt {retries}/{CONFIG["max_retries"]}): {e}. Retrying ...')
            if retries < CONFIG['max_retries']:
                print('Retrying in two seconds...')
                time.sleep(2)
            else:
                print('Max retries reached. Please check your API key and network connection.')
                sys.exit(1)

Enter fullscreen mode Exit fullscreen mode


Publishing the Project

The final stage involved preparing the project for GitHub.

I:

  1. Initialized Git

  2. Verified .env was ignored

  3. Generated a requirements file

  4. Wrote a README

  5. Added setup instructions and usage examples

  6. Published the repository

Writing the documentation was a useful reminder that shipping a project involves more than getting the code to work. Other developers need enough context to understand, install, and use what you’ve built.


What I Learned

This project started as an API integration exercise, but it ended up teaching me much more.

A few takeaways stand out:

  • Prompt design directly affects application behavior.

  • argparse makes a huge difference when building CLI tools.

  • Configuration becomes easier to manage when it’s centralized.

  • Even small projects benefit from retries, timeouts, and clear error messages.

  • Building a project exposes knowledge gaps much faster than reading about one.

The tool itself is still evolving, and there are plenty of improvements I want to make. For now, I’m happy to have something working, documented, and published.

Most importantly, I’m happy to continue building and sharing what I learn along the way.


GitHub logo Adejuwonvictor / AI-Powered-CLI-Tool

a command-line tool that takes natural language input and generates shell commands or scripts

devops-ai

An AI-powered command-line tool that converts plain English into Linux and DevOps shell commands Built with Python and the Google Gemini API.


What It Does

  • Converts plain English descriptions into shell commands
  • Explains what any shell command does with the --explain flag
  • Retries automatically on network failures
  • AI model is configurable via a single config file

Demo

$ python3 devops_ai.py "find all files larger than 100mb"
Command: find / -type f -size +100M

$ python3 devops_ai.py --explain "ps aux | grep python"
The ps aux command lists all currently running processes with details
like CPU and memory usage. The output is piped into grep which filters
and shows only lines containing the word python.
Enter fullscreen mode Exit fullscreen mode

Tech Stack

  • Python 3
  • Google Gemini API (gemini-2.5-flash-lite)
  • python-dotenv
  • argparse

Setup

1. Clone the repository

git clone https://github.com/Adejuwonvictor/AI-Powered-CLI-Tool
cd devops-ai
Enter fullscreen mode Exit fullscreen mode

2. Create and activate a virtual environment

python3 -m
Enter fullscreen mode Exit fullscreen mode

Top comments (0)