pls

A command-line tool that uses AI to generate and execute terminal commands from natural language descriptions.

Overview

pls is a terminal assistant powered by local AI (via Ollama). It translates plain English requests into executable shell commands and can answer technical questions.

While originally developed for macOS, the codebase is pure Python. It can be easily adapted for Linux or Windows subsystems with minor tweaks to the system-specific commands.

System Requirements

  • Operating System: macOS (recommended) or Linux
  • Python: Version 3.8 or higher
  • Ollama: Must be installed and running locally
  • Hardware: 8GB RAM minimum (16GB recommended for faster inference)

Installation

1. Prepare Environment

pip install ollama rich requests
ollama pull llama3.1:8b

2. Install Script

Download the source code (bottom of page), save it as pls, and make it executable.

chmod +x pls
sudo mv pls /usr/local/bin/

Configuration

You can customize the script by editing the variables at the top of the file.

Changing the AI Model

By default, it uses llama3.1:8b. To use a different model (like Ministral or codellama), simply pull the model in Ollama and update the script:

# Inside the script:
MODEL = "Ministral-3"  # Change this to your preferred model

Using on Linux/Windows

If running on non-macOS systems, you may need to manually adjust specific system calls within the script (e.g., changing open to xdg-open).

Usage

Command Mode

Generate and execute commands:

pls "find all python files in current directory"
pls "compress Documents folder"

Ask Mode

Ask technical questions without executing code:

pls -a "how do I change file permissions?"
pls -a "difference between chmod and chown"

Source Code

Save the following content to a file named pls.


#!/usr/bin/env python3
import sys
import time
import subprocess
import signal
import warnings
import requests
import ollama
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
from rich.style import Style

# --- CONFIGURATION ---
MODEL = "llama3.1:8b"
OLLAMA_API_URL = "http://127.0.0.1:11434"
console = Console()

# --- SILENCE WARNINGS ---
warnings.filterwarnings("ignore", message=".*urllib3 v2 only supports OpenSSL.*")

def cleanup_zombies():
    subprocess.run(["pkill", "-9", "-f", "ollama"], 
                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def is_ollama_running():
    try:
        requests.get(OLLAMA_API_URL, timeout=0.5)
        return True
    except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout):
        return False

def start_ollama():
    # Use Rich's status spinner
    with console.status(f"[bold yellow]⚡ Waking up {MODEL}...[/bold yellow]", spinner="dots"):
        process = subprocess.Popen(
            ["ollama", "serve"], 
            stdout=subprocess.DEVNULL, 
            stderr=subprocess.DEVNULL
        )
        retries = 0
        while not is_ollama_running():
            time.sleep(0.5)
            retries += 1
            if retries > 20:
                console.print("[bold red]Error: Could not start Ollama.[/bold red]")
                sys.exit(1)
    return process

def stop_ollama(process):
    if process:
        process.terminate()
        try:
            process.wait(timeout=2)
        except subprocess.TimeoutExpired:
            process.kill()

def sanitize_command(raw: str) -> str:
    raw = raw.strip()

    # 1. Remove markdown code blocks (```bash ... ```)
    if "```" in raw:
        lines = raw.splitlines()
        # Keep only lines that are NOT the ``` markers
        lines = [ln for ln in lines if not ln.strip().startswith("```")]
        # Re-join them
        raw = "\n".join(lines).strip()

    # 2. Remove inline backticks (`command`)
    if raw.startswith("`") and raw.endswith("`") and len(raw) > 2:
        raw = raw[1:-1].strip()

    # 3. Return the FULL string, not just the first line
    return raw

# --- MAIN LOGIC ---
server_process = None

def signal_handler(sig, frame):
    console.print(f"\n[bold red]🛑 Interrupted! Shutting down...[/bold red]")
    if server_process:
        stop_ollama(server_process)
    sys.exit(0)

def main():
    global server_process
    signal.signal(signal.SIGINT, signal_handler)

    # --- DETECT MODE ---
    ask_mode = False
    if "-a" in sys.argv:
        ask_mode = True
        sys.argv.remove("-a")

    if len(sys.argv) < 2:
        console.print(f"[bold]Usage:[/bold] {sys.argv[0]} [-a] 'your query here'")
        sys.exit(1)

    query = " ".join(sys.argv[1:])

    # Clean start logic
    if not is_ollama_running():
        cleanup_zombies()
        
    was_already_running = is_ollama_running()

    try:
        if not was_already_running:
            server_process = start_ollama()
        
        # --- SYSTEM PROMPT ---
        if ask_mode:
            system_instruction = (
                "You are a concise macOS (Apple Silicon) assistant. "
                "Answer clearly and accurately."
            )
        else:
            system_instruction = (
                "You are an expert macOS (Apple Silicon) terminal assistant. "
                "Output ONLY the raw, single-line command string. No markdown. No explanations.\n"
                "GUIDELINES:\n"
                "1. TOOLS: Use modern native tools (pmset for battery, ipconfig for IP, diskutil, etc). "
                "Avoid Linux-specific flags (e.g. use 'grep -E', never 'grep -P').\n"
                "2. FORMAT: Chain commands with '&&' or '|' into a single line. "
                "Be robust with regex (match units like '%' to avoid false positives).\n"
                "3. OUTPUT: Print to STDOUT by default. Only use '| pbcopy' if the user explicitly asks to 'copy' or 'clip'."
            )

        # --- THE THINKING ANIMATION ---
        content = ""
        with console.status(f"[bold cyan]🧠 {MODEL} is thinking...[/bold cyan]", spinner="aesthetic"):
            response = ollama.chat(
                model=MODEL,
                messages=[
                    {'role': 'system', 'content': system_instruction},
                    # --- TEACH BY EXAMPLE ---
                    {'role': 'user', 'content': 'get battery'},
                    {'role': 'assistant', 'content': 'pmset -g batt | grep -o "[0-9]*%"'},
                    {'role': 'user', 'content': 'copy ip address'},
                    {'role': 'assistant', 'content': 'ipconfig getifaddr en0 | pbcopy'},
                    # --- REAL QUERY ---
                    {'role': 'user', 'content': query}
                ],
                options={"keep_alive": 0} 
            )
            content = response['message']['content'].strip()

        # --- DISPLAY OUTPUT ---
        if ask_mode:
            console.print(Panel(content, title="[bold cyan]Answer[/bold cyan]", border_style="cyan"))
        else:
            command = sanitize_command(content)
            if not command:
                console.print("[bold red]Error:[/bold red] Model returned an empty command.")
                return

            # Highlight the command code
            syntax = Syntax(command, "bash", theme="monokai", line_numbers=False)
            console.print(Panel(syntax, title="[bold green]Suggested Command[/bold green]", border_style="green"))
            
            confirm = console.input(f"[grey50][?][/grey50] Run this? ([bold]Y[/bold]/n): ").lower()
            if confirm in ['', 'y', 'yes']:
                subprocess.run(command, shell=True)
            else:
                console.print("[dim]Aborted.[/dim]")

    except Exception as e:
        console.print(f"[bold red]Error:[/bold red] {e}")

    finally:
        if not was_already_running and server_process:
            stop_ollama(server_process)
            console.print(f"[dim]💤 {MODEL} put back to sleep.[/dim]")

if __name__ == "__main__":
    main()
            

Support & Contributing

If you find this tool helpful, please consider starring the project or sponsoring the developer.

⚠️ Important Disclaimer:
This tool uses Artificial Intelligence to generate system commands. While "Safe Mode" is enabled by default, AI can make mistakes. Always review the generated command carefully before confirming execution. The authors are not responsible for unintended data loss or system changes.