Building an Interactive AI Chat in Your ZSH Terminal
Ever wanted to chat with any AI model directly from your terminal without leaving your workflow? I built a lightweight ZSH function that connects to any OpenAI-compatible API with minimal dependencies—just curl and jq. I use fuelix.ai as my provider, which gives me access to dozens of models from OpenAI, Anthropic, Google, and others through a single API. But the function works with any OpenAI-compatible endpoint—local models, direct provider APIs, or custom services.
The Problem
While there are many web-based and desktop AI chat applications, constantly switching contexts between your terminal and browser breaks your flow. Most CLI tools are locked you into a specific provider, or looking at you open-claw, have way to much access to the system. I wanted something that:
- Works entirely in the terminal
- Has minimal dependencies (just shell, curl, and jq)
- Works with any OpenAI-compatible API
- Maintains conversation context
- Supports both interactive and one-off queries
- Feels natural to use
The Solution
I created a chat() function for ZSH that provides an interactive AI chat session with full conversation history, readline editing capabilities, and a clean interface. I use it with fuelix.ai, which lets me switch between GPT-4, Claude, Gemini, and dozens of other models on the fly. But it’s provider-agnostic—works with OpenAI, Anthropic via proxies, Ollama, or any custom OpenAI-compatible endpoint.
How It Works
The function uses several ZSH features to create a polished experience:
vared: ZSH’s built-in variable editor provides readline-style editing with history and cursor movement- Custom Keymaps: A custom keymap handles Ctrl+D to exit gracefully
- jq for JSON: Message history is stored as JSON and manipulated with
jq - Temporary Files: Conversation history is kept in a temp file that’s automatically cleaned up
- ANSI Colors:
print -Pwith ZSH color codes creates the colored prompts
The API integration uses curl to call any OpenAI-compatible endpoint, sending the full conversation history with each request. No SDKs, no pip installs, no npm packages—just standard Unix tools.
Usage Examples
Quick question:
$ chat what\'s the difference between zsh and bash in one sentence
$ Zsh is a more feature-rich, highly customizable shell with advanced completion and prompt/theming support, while Bash is the more widely defaulted, POSIX-oriented shell focused on compatibility and scripting portability.
Interactive session:
$ chat
Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset.
Type '/model <name>' to change the model (current: gpt-5.2).
-----------------------------------
You: How do I find all Python files modified in the last day?
AI: Use: find . -name "*.py" -mtime 0
You: What about excluding virtual environments?
AI: Add -not -path "*/venv/*": find . -name "*.py" -mtime 0 -not -path "*/venv/*"
You: /q
$
The Complete Code
chat() {
local history_file=$(mktemp)
local model="gpt-5.2"
# Disable syntax highlighting for vared (fixes red text)
local ZSH_HIGHLIGHT_HIGHLIGHTERS=()
# Create a custom widget to handle Ctrl+D
_chat_exit_widget() {
BUFFER="/q"
zle accept-line
}
zle -N _chat_exit_widget
# Create a custom keymap for chat
bindkey -N chat_keymap main
bindkey -M chat_keymap "^D" _chat_exit_widget
# Trap to clean up history file on exit
trap "rm -f $history_file" EXIT INT TERM
# Initialize history array
echo "[]" >| "$history_file"
# Helper to add message to history
_chat_add_message() {
local role="$1"
local content="$2"
local tmp=$(mktemp)
# Use jq to append to the JSON array in the file
jq --arg role "$role" --arg content "$content" \
'. + [{role: $role, content: $content}]' "$history_file" >| "$tmp" && mv "$tmp" "$history_file"
}
# Helper to make API call
_chat_call_api() {
local payload=$(jq -n --arg model "$model" --slurpfile messages "$history_file" '{model: $model, messages: $messages[0]}')
# Call API
local response=$(curl -s "$FUELIX_BASE_URL/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $FUELIX_API_KEY" \
-d "$payload")
# Parse response
local content=$(printf '%s' "$response" | jq -r '.choices[0].message.content // empty')
if [[ -z "$content" ]]; then
local error=$(printf '%s' "$response" | jq -r '.error.message // "Unknown error"')
echo "Error: $error"
return 1
else
echo "$content"
_chat_add_message "assistant" "$content"
return 0
fi
}
# If arguments provided, treat as one-off question
if [[ -n "$@" ]]; then
_chat_add_message "user" "$*"
_chat_call_api
rm -f "$history_file"
trap - EXIT INT TERM
return
fi
# Interactive mode
clear
echo "Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset."
echo "Type '/model <name>' to change the model (current: $model)."
echo "-----------------------------------"
local user_input=""
while true; do
# Use vared for readline-like input editing
# -p is prompt, -c creates variable if needed
# Use custom keymap to handle Ctrl+D
if ! vared -M chat_keymap -p "%F{green}You:%f " -c user_input; then
echo ""
break
fi
# Check for exit command
if [[ "$user_input" == "bye" || "$user_input" == "exit" || "$user_input" == "quit" || "$user_input" == "/q" ]]; then
break
fi
# Check for reset command
if [[ "$user_input" == "/new" ]]; then
echo "[]" >| "$history_file"
clear
echo "Chat started. Type '/q' or Ctrl+D to exit, '/new' to reset."
echo "Type '/model <name>' to change the model (current: $model)."
echo "-----------------------------------"
user_input=""
continue
fi
# Check for model command
if [[ "$user_input" == "/model"* ]]; then
local new_model=$(echo "$user_input" | cut -d' ' -f2-)
if [[ -n "$new_model" && "$new_model" != "/model" ]]; then
model="$new_model"
echo "Model changed to: $model"
else
echo "Current model: $model"
fi
user_input=""
continue
fi
# Skip empty input
if [[ -z "${user_input// }" ]]; then
continue
fi
_chat_add_message "user" "$user_input"
print -nP "%F{blue}AI:%f ...\r"
# Capture output to print it cleanly
local result
result=$(_chat_call_api)
# Clear the "..." line
print -n "\r\033[K"
print -P "%F{blue}AI:%f $result"
echo ""
# Reset input for next loop
user_input=""
done
rm -f "$history_file"
trap - EXIT INT TERM
}
Setup
To use this function, you’ll need:
- Dependencies: Install
jqfor JSON manipulation (brew install jqon macOS, orapt install jqon Linux) - Environment Variables: Point to any OpenAI-compatible API
- Add to Shell: Place the function in your
~/.zshrcor source it from a separate file
Fuelix.ai:
export FUELIX_BASE_URL="https://api.fuelix.ai/v1"
export FUELIX_API_KEY="fx-..."
# Now use /model gpt-4o, /model claude-3-5-sonnet, /model gemini-pro, etc.