Initial commit: Lightweight AI CLI tool for OpenRouter API

Vladimir Prelovac created

Change summary

README.md  | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ask        | 201 ++++++++++++++++++++++++++++++++++++++++++++++++
install.sh |  58 ++++++++++++++
3 files changed, 489 insertions(+)

Detailed changes

README.md 🔗

@@ -0,0 +1,230 @@
+# ask - AI CLI tool
+
+A lightweight bash script for querying AI models via the OpenRouter API, optimized for direct, executable output.
+
+## Features
+
+- **Direct Output** - Returns executable commands and answers without markdown formatting
+- **Multiple Models** - Quick access to Mercury Coder, Gemini, Claude Sonnet, Kimi, and Qwen models
+- **Streaming Support** - Real-time response streaming for long outputs
+- **Provider Routing** - Automatic fallback between providers for reliability
+- **Performance Metrics** - Shows response time and tokens/second
+- **Pipe Support** - Works seamlessly with Unix pipes and stdin
+
+## Quick start
+
+```bash
+# Clone and setup
+git clone https://github.com/yourusername/ask.git
+cd ask
+chmod +x ask
+
+# Set your API key
+export OPENROUTER_API_KEY="your-api-key-here"
+
+# Test it
+./ask "What is 2+2?"
+```
+
+## Installation
+
+### Option 1: Using install.sh (recommended)
+```bash
+sudo ./install.sh
+```
+
+### Option 2: Manual installation
+```bash
+chmod +x ask
+sudo cp ask /usr/local/bin/
+```
+
+### Persistent API key setup
+
+Add to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
+```bash
+export OPENROUTER_API_KEY="your-api-key-here"
+```
+
+## Usage
+
+### Basic usage
+
+```bash
+ask "What is 2+2?"
+ask "Write a Python hello world"
+```
+
+### Model selection
+
+```bash
+# Default model (Mercury Coder - optimized for code)
+ask "Write a Python function"
+
+# Shorthand flags for quick model switching
+ask -c "prompt"  # Mercury Coder (default, best for code)
+ask -g "prompt"  # Gemini 2.5 Flash (fast, general purpose)
+ask -s "prompt"  # Claude Sonnet 4 (complex reasoning)
+ask -k "prompt"  # Kimi K2 (long context)
+ask -q "prompt"  # Qwen 235B (large model)
+
+# Custom model by full name
+ask -m "openai/gpt-4o" "Explain this concept"
+```
+
+### Provider routing
+
+Specify provider order for fallback support:
+
+```bash
+ask --provider "openai,together" "Generate code"
+```
+
+This will try OpenAI first, then fall back to Together if needed.
+
+### System prompts
+
+```bash
+# Custom system prompt
+ask --system "You are a pirate" "Tell me about sailing"
+
+# Disable system prompt for raw model behavior
+ask -r "What is 2+2?"
+```
+
+### Streaming mode
+
+Get responses as they're generated:
+
+```bash
+ask --stream "Tell me a long story"
+```
+
+### Pipe input
+
+```bash
+echo "Fix this code: print('hello world)" | ask
+cat script.py | ask "Review this code"
+```
+
+## Options
+
+| Option | Description |
+|--------|-------------|
+| `-c` | Use Mercury Coder (default) |
+| `-g` | Use Google Gemini 2.5 Flash |
+| `-s` | Use Claude Sonnet 4 |
+| `-k` | Use Moonshotai Kimi K2 |
+| `-q` | Use Qwen3 235B |
+| `-m MODEL` | Use custom model |
+| `-r` | Disable system prompt |
+| `--stream` | Enable streaming output |
+| `--system` | Set custom system prompt |
+| `--provider` | Set provider order (comma-separated) |
+| `-h, --help` | Show help message |
+
+## Common use cases
+
+### Command generation
+```bash
+# Get executable commands directly
+ask "Command to find files larger than 100MB"
+# Output: find . -type f -size +100M
+
+ask "ffmpeg command to convert mp4 to gif"
+# Output: ffmpeg -i input.mp4 -vf "fps=10,scale=320:-1:flags=lanczos" output.gif
+```
+
+### Code generation
+```bash
+# Generate code snippets
+ask "Python function to calculate factorial"
+
+# Code review
+cat script.py | ask "Find potential bugs in this code"
+```
+
+### Quick answers
+```bash
+# Calculations
+ask "What is 18% of 2450?"
+# Output: 441
+
+# Technical questions
+ask "What port does PostgreSQL use?"
+# Output: 5432
+```
+
+### Advanced usage
+```bash
+# Chain commands
+ask "List all Python files" | ask "Generate a script to check syntax of these files"
+
+# Use with other tools
+docker ps -a | ask "Which containers are using the most memory?"
+
+# Provider fallback for reliability
+ask --provider "anthropic,openai" "Complex analysis task"
+```
+
+## Requirements
+
+### Dependencies
+- `bash` - Shell interpreter
+- `curl` - HTTP requests to OpenRouter API
+- `jq` - JSON parsing for API responses
+- `bc` - Performance metrics calculation
+
+### API access
+- OpenRouter API key (get one at [openrouter.ai](https://openrouter.ai))
+- Set as environment variable: `OPENROUTER_API_KEY`
+
+## Performance
+
+The tool displays performance metrics after each query:
+- **Model** - Which AI model processed the request  
+- **Provider** - The infrastructure provider that served it
+- **Response Time** - Total time in seconds
+- **Token Speed** - Generation speed in tokens/second
+
+Example output:
+```
+$ ask "What is 2+2?"
+
+4
+
+[inception/mercury-coder via Inception - 0.82s - 11.0 tok/s]
+```
+
+## Troubleshooting
+
+### API key not set
+```bash
+Error: OPENROUTER_API_KEY environment variable is not set
+# Solution: export OPENROUTER_API_KEY="your-key-here"
+```
+
+### Missing dependencies
+```bash
+# Check for required tools
+which curl jq bc
+
+# Install on macOS
+brew install jq bc
+
+# Install on Ubuntu/Debian
+sudo apt-get install jq bc
+```
+
+### No response or errors
+```bash
+# Test with verbose curl output
+curl -v https://openrouter.ai/api/v1/chat/completions \
+  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
+  -H "Content-Type: application/json" \
+  -d '{"model":"google/gemini-2.5-flash","messages":[{"role":"user","content":"test"}]}'
+```
+
+## License
+
+MIT

ask 🔗

@@ -0,0 +1,201 @@
+#!/bin/bash
+
+# ask - Simple OpenRouter API CLI tool
+# Usage: ask [OPTIONS] [PROMPT]
+
+set -euo pipefail
+
+# Check for API key
+if [ -z "${OPENROUTER_API_KEY:-}" ]; then
+    echo "Error: OPENROUTER_API_KEY environment variable is not set" >&2
+    exit 1
+fi
+
+# Model shortcuts function
+get_model() {
+    case "$1" in
+        c) echo "inception/mercury-coder:nitro" ;;
+        g) echo "google/gemini-2.5-flash:nitro" ;;
+        s) echo "anthropic/claude-sonnet-4:nitro" ;;
+        k) echo "moonshotai/kimi-k2:nitro" ;;
+        q) echo "qwen/qwen3-235b-a22b-2507:nitro" ;;
+    esac
+}
+
+# Default values
+MODEL="inception/mercury-coder"
+SYSTEM_PROMPT=""
+PROMPT=""
+STREAMING=false
+NO_SYSTEM=false
+PROVIDER_ORDER=""
+
+# Default system prompt (direct answers)
+DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information.
+
+For commands: Output executable syntax only. No explanations, no comments.
+For questions: Output the answer only. No context, no elaboration.
+
+Rules:
+- If asked for a command, provide ONLY the command
+- If asked a question, provide ONLY the answer
+- Never include markdown formatting or code blocks
+- Never add explanatory text before or after
+- Assume output will be piped or executed directly
+- For multi-step commands, use && or ; to chain them
+- Make commands robust and handle edge cases silently"
+
+# Function to show help
+show_help() {
+    cat << EOF
+ask - Query AI models via OpenRouter API
+
+Usage: ask [OPTIONS] [PROMPT]
+
+Options:
+  -c          Use inception/mercury-coder (default)
+  -g          Use google/gemini-2.5-flash
+  -s          Use anthropic/claude-sonnet-4
+  -k          Use moonshotai/kimi-k2
+  -q          Use qwen/qwen3-235b-a22b-2507
+  -m MODEL    Use custom model
+  -r          Disable system prompt (raw model behavior)
+  --stream    Enable streaming output
+  --system    Set system prompt for the conversation
+  --provider  Comma-separated list of providers for routing
+  -h, --help  Show this help message
+
+Examples:
+  ask "Write a hello world in Python"
+  ask -g "Explain quantum computing"
+  ask -m openai/gpt-4o "What is 2+2?"
+  echo "Fix this code" | ask
+  ask --system "You are a pirate" "Tell me about sailing"
+
+EOF
+    exit 0
+}
+
+# Parse command line arguments
+while [ $# -gt 0 ]; do
+    case "$1" in
+        -h|--help) show_help ;;
+        -[cgskq])
+            MODEL="$(get_model "${1:1}")"
+            shift ;;
+        -m)
+            MODEL="${2:?Error: -m requires a model name}"
+            shift 2 ;;
+        -r)
+            NO_SYSTEM=true
+            shift ;;
+        --stream)
+            STREAMING=true
+            shift ;;
+        --system)
+            SYSTEM_PROMPT="${2:?Error: --system requires a prompt}"
+            shift 2 ;;
+        --provider)
+            PROVIDER_ORDER="${2:?Error: --provider requires providers}"
+            shift 2 ;;
+        *)
+            PROMPT="$*"
+            break ;;
+    esac
+done
+
+# If no prompt provided as argument, read from stdin
+if [ -z "$PROMPT" ]; then
+    if [ -t 0 ]; then
+        echo "Error: No prompt provided. Use 'ask -h' for help." >&2
+        exit 1
+    fi
+    # Read all stdin, preserving multi-line format
+    PROMPT=$(cat)
+fi
+
+# Apply default system prompt unless disabled or custom prompt provided
+if [ "$NO_SYSTEM" = false ] && [ -z "$SYSTEM_PROMPT" ]; then
+    SYSTEM_PROMPT="$DEFAULT_PROMPT"
+fi
+
+# Build messages array with proper JSON escaping
+if [ -n "$SYSTEM_PROMPT" ]; then
+    MESSAGES='[{"role":"system","content":'"$(printf '%s' "$SYSTEM_PROMPT" | jq -Rs .)"'},{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]'
+else
+    MESSAGES='[{"role":"user","content":'"$(printf '%s' "$PROMPT" | jq -Rs .)"'}]'
+fi
+
+# Record start time
+START_TIME=$(date +%s.%N)
+
+# Build JSON payload once
+PROVIDER_JSON=""
+if [ -n "$PROVIDER_ORDER" ]; then
+    PROVIDER_JSON=',"provider":{"order":['$(echo "$PROVIDER_ORDER" | awk -F, '{for(i=1;i<=NF;i++) printf "\"%s\"%s", $i, (i<NF?",":"")}')']}'
+fi
+
+JSON_PAYLOAD='{
+    "model": "'"$MODEL"'",
+    "messages": '"$MESSAGES"',
+    "stream": '$([ "$STREAMING" = true ] && echo true || echo false)"$PROVIDER_JSON"'
+}'
+
+API_URL="https://openrouter.ai/api/v1/chat/completions"
+
+# Add newline before answer
+echo
+
+# Make API request
+if [ "$STREAMING" = true ]; then
+    # Streaming mode
+    curl -sS "$API_URL" \
+      -H "Content-Type: application/json" \
+      -H "Authorization: Bearer $OPENROUTER_API_KEY" \
+      -d "$JSON_PAYLOAD" 2>&1 | while IFS= read -r line; do
+        # Check for errors
+        if echo "$line" | grep -q '"error"'; then
+            echo "Error: $(echo "$line" | jq -r '.error.message // .error // "Unknown error"')" >&2
+            exit 1
+        fi
+
+        # Process SSE data lines
+        if [[ "$line" == data:* ]]; then
+            json="${line#data: }"
+            [ "$json" = "" ] || [ "$json" = "[DONE]" ] && continue
+            
+            content=$(echo "$json" | jq -r '.choices[0].delta.content // ""' 2>/dev/null)
+            [ -n "$content" ] && printf '%s' "$content"
+        fi
+    done
+    echo
+    
+    # Show metadata
+    ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc))
+    echo
+    echo "[$MODEL - ${ELAPSED}s]" >&2
+else
+    # Non-streaming mode
+    response=$(curl -sS "$API_URL" \
+      -H "Content-Type: application/json" \
+      -H "Authorization: Bearer $OPENROUTER_API_KEY" \
+      -d "$JSON_PAYLOAD" 2>&1)
+
+    # Check for errors
+    if echo "$response" | grep -q '"error"'; then
+        echo "Error: $(echo "$response" | jq -r '.error.message // .error // "Unknown error"')" >&2
+        exit 1
+    fi
+
+    # Extract and print content
+    echo "$response" | jq -r '.choices[0].message.content // "No response received"'
+
+    # Show metadata
+    ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc))
+    TOKENS=$(echo "$response" | jq -r '.usage.completion_tokens // 0')
+    PROVIDER=$(echo "$response" | jq -r '.provider // "Unknown"')
+    TPS=$(echo "scale=1; $TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0")
+    
+    echo
+    echo "[$MODEL via $PROVIDER - ${ELAPSED}s - ${TPS} tok/s]" >&2
+fi

install.sh 🔗

@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# install.sh - Install ask CLI tool system-wide
+
+set -euo pipefail
+
+# Colors
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+echo "Installing 'ask' CLI tool..."
+
+# Check if script exists
+if [ ! -f "ask" ]; then
+    echo "Error: 'ask' script not found in current directory" >&2
+    exit 1
+fi
+
+# Make executable
+chmod +x ask
+
+# Install to /usr/local/bin
+echo "Installing to /usr/local/bin/ask (requires sudo)..."
+sudo cp ask /usr/local/bin/
+
+echo -e "${GREEN}✓ Installation complete!${NC}"
+echo
+
+# Get OpenRouter IPs
+echo "Resolving OpenRouter DNS..."
+IPS=$(dig +short openrouter.ai 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || nslookup openrouter.ai 2>/dev/null | grep -A1 "Name:" | grep "Address:" | awk '{print $2}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
+
+if [ -n "$IPS" ]; then
+    echo -e "${YELLOW}OpenRouter IP addresses:${NC}"
+    echo "$IPS"
+    echo
+    echo "To improve performance, you can add these to /etc/hosts:"
+    echo "----------------------------------------"
+    for IP in $IPS; do
+        echo "$IP    openrouter.ai"
+    done | head -1  # Only show first IP as hosts file needs single entry
+    echo "----------------------------------------"
+    echo
+    echo "Add with: sudo nano /etc/hosts"
+    echo "(Only add ONE IP address to avoid conflicts)"
+else
+    echo "Could not resolve OpenRouter IPs. Network may be unavailable."
+fi
+
+echo
+echo -e "${GREEN}Usage:${NC}"
+echo "  ask 'What is 2+2?'"
+echo "  ask -g 'Explain quantum computing'"
+echo "  ask --help"
+echo
+echo "Don't forget to set your API key:"
+echo "  export OPENROUTER_API_KEY='your-key-here'"