Posted on
Artificial Intelligence

Build an Artificial Intelligence Bash Assistant from Scratch

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Build an Artificial Intelligence Bash Assistant from Scratch

What if your terminal could answer questions, draft one-liners, and explain cryptic errors without leaving your shell? In a world of endless docs, man pages, and forum tabs, an AI Bash assistant can keep you focused and productive, turning “What’s the sed for that?” into a one-liner you can refine in seconds.

This guide shows you how to build a practical, privacy-aware Bash assistant that runs entirely from your terminal. You choose the backend: a cloud model (OpenAI-compatible) or a local model (Ollama). No heavyweight frameworks—just Bash, curl, and jq.

Why this is worth your time

  • Keep your hands on the keyboard: Ask and get answers in your workflow.

  • Control and privacy: Use a local model when you can, and the cloud when you must.

  • Portable and simple: One small Bash script; works across distros.

  • Extensible: Add context memory, modes (explain, refactor, draft), and guardrails.

What you’ll build

  • A single ai Bash script

  • Supports either:

    • OpenAI-compatible HTTP API, or
    • A local Ollama model via its HTTP API
  • Modes for general chat and command explanation

  • Optional lightweight conversation memory


1) Install prerequisites

You’ll need curl and jq.

  • Debian/Ubuntu (apt):

    sudo apt update
    sudo apt install -y curl jq
    
  • Fedora/RHEL/CentOS (dnf):

    sudo dnf install -y curl jq
    
  • openSUSE (zypper):

    sudo zypper refresh
    sudo zypper install -y curl jq
    

Optional (local backend): Install Ollama to run models on your machine.

curl -fsSL https://ollama.com/install.sh | sh
# Start the server (if not started automatically) and pull a model
ollama pull llama3

By default Ollama listens on http://127.0.0.1:11434.


2) Choose your backend

Pick one:

  • OpenAI-compatible (cloud):

    • Get an API key from your provider.
    • Export environment variables:
    export OPENAI_API_KEY="your_api_key_here"
    export OPENAI_BASE_URL="https://api.openai.com/v1"   # default; change if using a compatible host
    export OPENAI_MODEL="gpt-4o-mini"                    # or another available model
    
  • Ollama (local):

    • Ensure Ollama is running.
    • Export environment variables:
    export OLLAMA_HOST="127.0.0.1:11434"  # default
    export OLLAMA_MODEL="llama3"          # or any model you've pulled
    

You can switch backends any time by setting AI_BACKEND=openai or AI_BACKEND=ollama.


3) Create the minimal AI assistant

Save this as ~/bin/ai (or ~/.local/bin/ai) and make it executable.

#!/usr/bin/env bash
set -euo pipefail

# Minimal, single-file AI assistant for Bash
# Backends: openai (cloud) or ollama (local)
AI_BACKEND="${AI_BACKEND:-openai}"  # openai | ollama
SYSTEM_PROMPT="${SYSTEM_PROMPT:-You are a concise Linux shell assistant. Favor short, correct answers with examples.}"
TMP_DIR="${XDG_RUNTIME_DIR:-/tmp}"
CONTEXT_FILE="${CONTEXT_FILE:-$TMP_DIR/ai_context.jsonl}"  # optional memory

die() { echo "error: $*" >&2; exit 1; }

usage() {
  cat <<EOF
Usage:
  ai [--explain] [--mem] "your question"
  echo "text" | ai [--explain] [--mem]

Options:
  --explain   Prefix the prompt with an instruction to explain, step-by-step.
  --mem       Use lightweight conversation memory (stores brief recent turns).

Backends:
  Set AI_BACKEND=openai (default) or AI_BACKEND=ollama.

OpenAI env (if AI_BACKEND=openai):
  OPENAI_API_KEY (required), OPENAI_BASE_URL (default: https://api.openai.com/v1), OPENAI_MODEL (e.g., gpt-4o-mini)

Ollama env (if AI_BACKEND=ollama):
  OLLAMA_HOST (default: 127.0.0.1:11434), OLLAMA_MODEL (e.g., llama3)
EOF
}

# Parse flags
EXPLAIN=0
USE_MEM=0
ARGS=()
while [[ $# -gt 0 ]]; do
  case "$1" in
    --explain) EXPLAIN=1; shift;;
    --mem) USE_MEM=1; shift;;
    -h|--help) usage; exit 0;;
    *) ARGS+=("$1"); shift;;
  esac
done

# Read input
if [[ ${#ARGS[@]} -gt 0 ]]; then
  USER_INPUT="${ARGS[*]}"
else
  if [ -t 0 ]; then usage; exit 1; fi
  USER_INPUT="$(cat)"
fi

if [[ $EXPLAIN -eq 1 ]]; then
  USER_INPUT="Explain, step-by-step, for an experienced Linux user: ${USER_INPUT}"
fi

json_escape() { jq -Rs . <<<"$1"; }  # returns a JSON string

build_messages_json() {
  # Build the messages array with optional short memory from JSONL
  local msgs="[ {\"role\":\"system\",\"content\":$(json_escape "$SYSTEM_PROMPT")} ]"
  if [[ $USE_MEM -eq 1 && -s "$CONTEXT_FILE" ]]; then
    # Take last 6 lines (3 turns), truncate each content to ~800 chars
    local recent
    recent="$(tail -n 6 "$CONTEXT_FILE" | sed -E 's/(.{0,800}).*/\1/')" || true
    if [[ -n "$recent" ]]; then
      while IFS= read -r line; do
        msgs="${msgs%,}] , $line ]"
      done <<<"$recent"
    fi
  fi
  msgs="${msgs%,}] , {\"role\":\"user\",\"content\":$(json_escape "$USER_INPUT")} ]"
  echo "$msgs"
}

call_openai() {
  : "${OPENAI_API_KEY:?Set OPENAI_API_KEY}"
  local base="${OPENAI_BASE_URL:-https://api.openai.com/v1}"
  local model="${OPENAI_MODEL:-gpt-4o-mini}"
  local messages
  messages="$(build_messages_json)"
  local payload
  payload="$(jq -n --arg m "$model" --argjson msgs "$messages" '{model:$m, messages:$msgs, temperature:0.2}')"
  curl -fsS "$base/chat/completions" \
    -H "Authorization: Bearer $OPENAI_API_KEY" \
    -H "Content-Type: application/json" \
    -d "$payload"
}

call_ollama() {
  local host="${OLLAMA_HOST:-127.0.0.1:11434}"
  local model="${OLLAMA_MODEL:-llama3}"
  local messages
  messages="$(build_messages_json)"
  local payload
  payload="$(jq -n --arg m "$model" --argjson msgs "$messages" '{model:$m, messages:$msgs, stream:false}')"
  curl -fsS "http://$host/api/chat" \
    -H "Content-Type: application/json" \
    -d "$payload"
}

# Dispatch
resp=""
case "$AI_BACKEND" in
  openai)  resp="$(call_openai)";  OUT="$(jq -r '.choices[0].message.content // empty' <<<"$resp")" ;;
  ollama)  resp="$(call_ollama)";  OUT="$(jq -r '.message.content // empty'           <<<"$resp")" ;;
  *) die "Unknown AI_BACKEND: $AI_BACKEND";;
esac

[[ -n "$OUT" ]] || { echo "$resp" >&2; die "No content returned. Check config/keys."; }

# Print the model's reply
printf "%s\n" "$OUT"

# Append to memory if requested
if [[ $USE_MEM -eq 1 ]]; then
  mkdir -p "$(dirname "$CONTEXT_FILE")"
  printf '{"role":"user","content":%s}\n'      "$(json_escape "$USER_INPUT")" >>"$CONTEXT_FILE"
  printf '{"role":"assistant","content":%s}\n' "$(json_escape "$OUT")"        >>"$CONTEXT_FILE"
fi

Make it executable and add to your PATH:

chmod +x ~/bin/ai
# If needed:
mkdir -p ~/.local/bin
cp ~/bin/ai ~/.local/bin/
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Test it:

AI_BACKEND=ollama ai "Write a one-liner to list the five largest files under /var, human-readable."
AI_BACKEND=openai ai --explain "sed -E 's/([0-9]{3})/[\1]/g' file.txt"

4) Real-world examples

  • Explain a command before you run it:

    ai --explain "find /etc -type f -name '*.conf' -mtime -3 -print"
    
  • Draft a safe cleanup script (you validate before running):

    ai "Draft a bash snippet that archives logs older than 14 days in /var/log/myapp to /var/log/archive using tar and gzip. Include a dry-run comment."
    
  • Turn a requirement into a one-liner:

    ai "Show a robust awk command to sum the 3rd column of a CSV (with header), ignoring blanks."
    
  • Troubleshoot:

    ai "systemd service keeps restarting with exit code 203/EXEC. What are the common causes and how do I diagnose?"
    

Tip: Pipe stderr or logs into ai --explain for quick context when possible.


5) Level up your assistant (optional enhancements)

  • Add a “shellsafe” instruction:

    • Set a stronger system prompt so the model avoids destructive actions:
    export SYSTEM_PROMPT="You are a cautious Linux assistant. Prefer read-only diagnostics. If a command could change data, present it with clear comments and a dry-run or noop variant."
    
  • Add a shorthand alias:

    echo "alias aii='AI_BACKEND=ollama ai --mem'" >> ~/.bashrc
    
  • Stream responses:

    • Both backends can stream. For a quick take, switch to stream:true (Ollama) or stream:true (OpenAI), then read line-by-line. Example sketch (advanced):
    # For OpenAI: add "stream": true and process .choices[0].delta.content chunks from the SSE lines.
    # For Ollama: add "stream": true and print each JSON line's .message.content incrementally.
    
  • Curate memory:

    • Periodically truncate $CONTEXT_FILE or keep per-project memories:
    export CONTEXT_FILE="$PWD/.ai_context.jsonl"
    
  • Guardrails:

    • Never execute returned commands blindly. Consider copying with the mouse or require a manual paste.
    • Prefer “explain” mode when exploring unfamiliar operations.

Troubleshooting

  • 401/403 with OpenAI backend:

    • Check OPENAI_API_KEY, OPENAI_BASE_URL, and model name.
  • 404/connection refused with Ollama:

    • Ensure Ollama is running and the model is pulled:
    ollama serve   # if needed
    ollama list
    ollama pull llama3
    
  • Empty replies:

    • Print raw response to debug:
    AI_BACKEND=openai ai "hello" 2>/dev/null | sed -n '1,40p'
    

Conclusion and Call to Action

You now have a fast, composable AI assistant that lives in your shell, respects your workflow, and works with either local or cloud models. Start small: ask it to explain commands or draft safe examples. Then extend it with streaming, project-scoped memory, or extra modes tailored to your work.

Your next step:

  • Pick a backend, set the env vars, and run your first query: AI_BACKEND=ollama ai --explain "What is the safest way to delete old Docker images?" If it saves you even one tab switch today, it’s already doing its job.