From ddf2e5dc85b68dfd080d9bef080a060b6209751f Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Mon, 22 Sep 2025 16:29:21 -0500 Subject: [PATCH 1/6] Use env !# to be more portable --- ask | 2 +- install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ask b/ask index 6ee2b3f..3f45f85 100755 --- a/ask +++ b/ask @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # ask - Simple OpenRouter API CLI tool # Usage: ask [OPTIONS] [PROMPT] diff --git a/install.sh b/install.sh index db9ec35..7e40519 100755 --- a/install.sh +++ b/install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # install.sh - Install ask CLI tool system-wide From ccbb4436a35316922b0cd1aba5e862242fd751b9 Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Thu, 27 Nov 2025 09:51:41 -0600 Subject: [PATCH 2/6] Refactor ask script to use Claude Code CLI instead of OpenRouter API Replace OpenRouter API integration with Claude Code CLI wrapper. Add system context gathering with 24-hour caching for OS, shell, package manager, dev tools, and project type. Replace model shortcuts with Claude variants (sonnet/opus/haiku). Add --debug flag for troubleshooting. Remove streaming and provider options. Simplify to single CLI execution instead of curl-based API calls. --- ask | 341 ++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 242 insertions(+), 99 deletions(-) diff --git a/ask b/ask index 3f45f85..264fda5 100755 --- a/ask +++ b/ask @@ -1,36 +1,37 @@ #!/usr/bin/env bash -# ask - Simple OpenRouter API CLI tool +# ask - Simple Claude Code CLI wrapper # 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 +# Check for Claude CLI +if ! command -v claude &> /dev/null; then + echo "Error: claude command not found. Install Claude Code CLI first." >&2 + echo "Visit: https://claude.com/claude-code" >&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-preview-09-2025:nitro" ;; - s) echo "anthropic/claude-sonnet-4.5:nitro" ;; - x) echo "x-ai/grok-code-fast-1:nitro" ;; - k) echo "moonshotai/kimi-k2:nitro" ;; - q) echo "qwen/qwen3-235b-a22b-2507:nitro" ;; - o) echo "openai/gpt-5:nitro" ;; + s) echo "sonnet" ;; + o) echo "opus" ;; + h) echo "haiku" ;; esac } +# System information caching +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ask" +CACHE_FILE="$CACHE_DIR/sysinfo" +CACHE_MAX_AGE=86400 # 24 hours in seconds + # Default values -MODEL="qwen/qwen3-235b-a22b-2507:nitro" +MODEL="sonnet" SYSTEM_PROMPT="" PROMPT="" -STREAMING=false NO_SYSTEM=false -PROVIDER_ORDER="" +DEBUG=false # Default system prompt (direct answers) DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information. @@ -50,41 +51,171 @@ Rules: # Function to show help show_help() { cat << EOF -ask - Query AI models via OpenRouter API +ask - Query Claude AI via Claude Code CLI Usage: ask [OPTIONS] [PROMPT] Options: - -c Use inception/mercury-coder - -g Use google/gemini-2.5-flash-preview-09-2025 - -s Use anthropic/claude-sonnet-4.5 - -x Use x-ai/grok-code-fast-1 - -k Use moonshotai/kimi-k2 - -q Use qwen/qwen3-235b-a22b-2507 (default) - -o Use openai/gpt-5 - -m MODEL Use custom model + -s Use Claude Sonnet (default) + -o Use Claude Opus + -h Use Claude Haiku + -m MODEL Use custom Claude 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 + --system Set custom system prompt for the conversation + --debug Show system context, prompts, and command (output to stderr) + --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?" + ask -o "Explain quantum computing" + ask -h "What is 2+2?" echo "Fix this code" | ask ask --system "You are a pirate" "Tell me about sailing" + ask -r "Tell me a joke" + ask --debug "Show me debug output" EOF exit 0 } +# Gather system information +gather_sysinfo() { + # OS and Distribution + local os_info=$(grep -E '^(NAME|VERSION_ID)=' /etc/os-release 2>/dev/null | \ + cut -d= -f2 | tr -d '"' | paste -sd ' ' 2>/dev/null || \ + echo "$(uname -s) $(uname -r)") + + # Architecture + local arch=$(uname -m) + + # Package Manager + local pkg_mgr="unknown" + for pm in apt dnf yum pacman nix brew; do + if command -v "$pm" >/dev/null 2>&1; then + pkg_mgr="$pm" + break + fi + done + + # Shell + local shell_name=$(basename "$SHELL" 2>/dev/null || echo "bash") + + # Development Tools + local tools="" + for tool in python3 node npm docker git make gcc nix pip cargo go java rustc; do + command -v "$tool" >/dev/null 2>&1 && tools="${tools}${tool}," + done + tools=${tools%,} # Remove trailing comma + + # Working Directory + local wd=$(pwd) + + # Git Repository + local in_git="no" + git rev-parse --is-inside-work-tree >/dev/null 2>&1 && in_git="yes" + + # Project Type + local proj_type="none" + if [ -f package.json ]; then proj_type="node" + elif [ -f requirements.txt ] || [ -f pyproject.toml ]; then proj_type="python" + elif [ -f Cargo.toml ]; then proj_type="rust" + elif [ -f go.mod ]; then proj_type="go" + elif [ -f pom.xml ] || [ -f build.gradle ]; then proj_type="java" + elif [ -f Makefile ]; then proj_type="make" + fi + + # Format output (one line per item for easy parsing) + cat << EOF +OS=$os_info +ARCH=$arch +PKG=$pkg_mgr +SHELL=$shell_name +TOOLS=$tools +WD=$wd +GIT=$in_git +PROJECT=$proj_type +EOF +} + +# Check if cache should be refreshed +should_refresh_cache() { + [ ! -f "$CACHE_FILE" ] && return 0 + + # Try to get file modification time + local cache_mtime + if cache_mtime=$(stat -c %Y "$CACHE_FILE" 2>/dev/null); then + : # GNU stat worked + elif cache_mtime=$(stat -f %m "$CACHE_FILE" 2>/dev/null); then + : # BSD stat worked + else + return 0 # Can't determine age, refresh + fi + + local now=$(date +%s) + local age=$((now - cache_mtime)) + + [ "$age" -gt "$CACHE_MAX_AGE" ] && return 0 + return 1 +} + +# Get system context (cached) +get_system_context() { + # Create cache directory if needed + if [ ! -d "$CACHE_DIR" ]; then + mkdir -p "$CACHE_DIR" 2>/dev/null || { + # Fallback to temp + CACHE_DIR="/tmp/ask-${USER:-default}" + CACHE_FILE="$CACHE_DIR/sysinfo" + mkdir -p "$CACHE_DIR" 2>/dev/null || return 1 + } + fi + + # Refresh cache if needed + if should_refresh_cache; then + gather_sysinfo > "$CACHE_FILE" 2>/dev/null || return 1 + fi + + # Read and format cache + if [ -f "$CACHE_FILE" ]; then + # Parse cache file + local os_info arch pkg_mgr shell_name tools wd in_git proj_type + + while IFS='=' read -r key value; do + case "$key" in + OS) os_info="$value" ;; + ARCH) arch="$value" ;; + PKG) pkg_mgr="$value" ;; + SHELL) shell_name="$value" ;; + TOOLS) tools="$value" ;; + WD) wd="$value" ;; + GIT) in_git="$value" ;; + PROJECT) proj_type="$value" ;; + esac + done < "$CACHE_FILE" + + # Format concise context + local context="System: $os_info $arch | Shell: $shell_name | Pkg: $pkg_mgr" + [ -n "$tools" ] && context="$context +Tools: $tools" + + local ctx_detail="Context: $wd" + [ "$in_git" = "yes" ] && ctx_detail="$ctx_detail | Git repo" + [ "$proj_type" != "none" ] && ctx_detail="$ctx_detail | ${proj_type} project" + context="$context +$ctx_detail" + + echo "$context" + fi +} + # Parse command line arguments while [ $# -gt 0 ]; do case "$1" in - -h|--help) show_help ;; - -[cgskqxo]) + --help) show_help ;; + --debug) + DEBUG=true + shift ;; + -[soh]) MODEL="$(get_model "${1:1}")" shift ;; -m) @@ -93,15 +224,9 @@ while [ $# -gt 0 ]; do -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 ;; @@ -118,88 +243,106 @@ if [ -z "$PROMPT" ]; then PROMPT=$(cat) fi +# Gather system context (unless in raw mode) +SYSTEM_CONTEXT="" +if [ "$NO_SYSTEM" = false ]; then + SYSTEM_CONTEXT=$(get_system_context) +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 .)"'}]' +# Prepend system context to system prompt +if [ -n "$SYSTEM_CONTEXT" ] && [ -n "$SYSTEM_PROMPT" ]; then + SYSTEM_PROMPT="${SYSTEM_CONTEXT} + +${SYSTEM_PROMPT}" 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&2 + if [ -n "$SYSTEM_CONTEXT" ]; then + echo "$SYSTEM_CONTEXT" >&2 + else + echo "(none)" >&2 + fi + echo >&2 + + echo "=== SYSTEM PROMPT ===" >&2 + if [ -n "$SYSTEM_PROMPT" ]; then + echo "$SYSTEM_PROMPT" >&2 + else + echo "(none)" >&2 + fi + echo >&2 + + echo "=== USER PROMPT ===" >&2 + echo "$PROMPT" >&2 + echo >&2 -JSON_PAYLOAD='{ - "model": "'"$MODEL"'", - "messages": '"$MESSAGES"', - "stream": '$([ "$STREAMING" = true ] && echo true || echo false)"$PROVIDER_JSON"' -}' + # Build debug command string + debug_cmd="claude -p \"\" --model \"$MODEL\"" + [ -n "$SYSTEM_PROMPT" ] && debug_cmd="$debug_cmd --system-prompt \"\"" + debug_cmd="$debug_cmd --output-format json" -API_URL="https://openrouter.ai/api/v1/chat/completions" + echo "=== CLAUDE COMMAND ===" >&2 + echo "$debug_cmd" >&2 + echo >&2 +fi # 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 +# Execute Claude CLI with proper argument passing +if [ -n "$SYSTEM_PROMPT" ]; then + # With custom system prompt + response=$(claude -p "$PROMPT" --model "$MODEL" --system-prompt "$SYSTEM_PROMPT" --output-format json 2>&1) +else + # Without system prompt (raw mode) + response=$(claude -p "$PROMPT" --model "$MODEL" --output-format json 2>&1) +fi - # Process SSE data lines - if [[ "$line" == data:* ]]; then - json="${line#data: }" - [ "$json" = "" ] || [ "$json" = "[DONE]" ] && continue +exit_code=$? - content=$(echo "$json" | jq -r '.choices[0].delta.content // ""' 2>/dev/null) - [ -n "$content" ] && printf '%s' "$content" - fi - done - echo +# Debug output for response +if [ "$DEBUG" = true ]; then + echo "=== RAW RESPONSE ===" >&2 + echo "$response" >&2 + echo >&2 +fi - # 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 +# Check for errors +if [ $exit_code -ne 0 ]; then + echo "Error: Claude CLI failed" >&2 + echo "$response" >&2 + exit 1 +fi - # Extract and print content - echo "$response" | jq -r '.choices[0].message.content // "No response received"' +# Extract and print content from JSON response +content=$(echo "$response" | jq -r '.result // .content // .response // .' 2>/dev/null) + +# If jq parsing failed or returned null, just print the raw response +if [ -z "$content" ] || [ "$content" = "null" ]; then + echo "$response" +else + echo "$content" +fi - # 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") +# Show metadata +ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc)) +INPUT_TOKENS=$(echo "$response" | jq -r '.input_tokens // 0' 2>/dev/null) +OUTPUT_TOKENS=$(echo "$response" | jq -r '.output_tokens // 0' 2>/dev/null) - echo - echo "[$MODEL via $PROVIDER - ${ELAPSED}s - ${TPS} tok/s]" >&2 +echo +if [ "$INPUT_TOKENS" != "0" ] && [ "$OUTPUT_TOKENS" != "0" ]; then + TPS=$(echo "scale=1; $OUTPUT_TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0") + echo "[$MODEL - ${ELAPSED}s - ${OUTPUT_TOKENS} tokens - ${TPS} tok/s]" >&2 +else + echo "[$MODEL - ${ELAPSED}s]" >&2 fi From 5600c6099e2a23c73c95526c0ba338f3aaef24fc Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Thu, 27 Nov 2025 09:59:33 -0600 Subject: [PATCH 3/6] Add --context flag to pipe stdin as context with prompt argument --- ask | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/ask b/ask index 264fda5..19aadd0 100755 --- a/ask +++ b/ask @@ -32,6 +32,7 @@ SYSTEM_PROMPT="" PROMPT="" NO_SYSTEM=false DEBUG=false +WITH_CONTEXT=false # Default system prompt (direct answers) DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information. @@ -62,14 +63,28 @@ Options: -m MODEL Use custom Claude model -r Disable system prompt (raw model behavior) --system Set custom system prompt for the conversation + --context Pipe stdin as context, combine with prompt argument --debug Show system context, prompts, and command (output to stderr) --help Show this help message Examples: + # Basic usage ask "Write a hello world in Python" ask -o "Explain quantum computing" ask -h "What is 2+2?" + + # Pipe input as prompt echo "Fix this code" | ask + + # Claude can run commands (recommended for git diffs) + ask "Look at git diff and write a commit message" + ask "Run git status and summarize changes" + + # Pipe context with --context flag (alternative) + git diff | ask --context "Write a commit message for this diff" + cat error.log | ask --context "Explain what caused this error" + + # Other options ask --system "You are a pirate" "Tell me about sailing" ask -r "Tell me a joke" ask --debug "Show me debug output" @@ -215,6 +230,9 @@ while [ $# -gt 0 ]; do --debug) DEBUG=true shift ;; + --context) + WITH_CONTEXT=true + shift ;; -[soh]) MODEL="$(get_model "${1:1}")" shift ;; @@ -233,10 +251,29 @@ while [ $# -gt 0 ]; do esac done -# If no prompt provided as argument, read from stdin -if [ -z "$PROMPT" ]; then +# Handle stdin and prompt +if [ "$WITH_CONTEXT" = true ]; then + # --context mode: read stdin as context, combine with prompt + if [ ! -t 0 ]; then + STDIN_CONTENT=$(cat) + if [ -z "$PROMPT" ]; then + echo "Error: --context requires both stdin and a prompt argument" >&2 + exit 1 + fi + # Combine stdin content with prompt + PROMPT="${STDIN_CONTENT} + +--- + +${PROMPT}" + else + echo "Error: --context requires stdin input. Use: command | ask --context \"prompt\"" >&2 + exit 1 + fi +elif [ -z "$PROMPT" ]; then + # No prompt argument: read from stdin if [ -t 0 ]; then - echo "Error: No prompt provided. Use 'ask -h' for help." >&2 + echo "Error: No prompt provided. Use 'ask --help' for help." >&2 exit 1 fi # Read all stdin, preserving multi-line format From e2d63448a2a846a25f364211ad850edd06610d18 Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Thu, 27 Nov 2025 10:05:11 -0600 Subject: [PATCH 4/6] Add --stats flag to show usage statistics (time, tool calls, tokens, cost) --- ask | 58 ++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/ask b/ask index 19aadd0..8b664dc 100755 --- a/ask +++ b/ask @@ -33,6 +33,7 @@ PROMPT="" NO_SYSTEM=false DEBUG=false WITH_CONTEXT=false +SHOW_STATS=false # Default system prompt (direct answers) DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information. @@ -64,6 +65,7 @@ Options: -r Disable system prompt (raw model behavior) --system Set custom system prompt for the conversation --context Pipe stdin as context, combine with prompt argument + --stats Show statistics (time, tool calls, tokens, cost) --debug Show system context, prompts, and command (output to stderr) --help Show this help message @@ -84,6 +86,10 @@ Examples: git diff | ask --context "Write a commit message for this diff" cat error.log | ask --context "Explain what caused this error" + # Show stats to see tool usage and cost + ask --stats "Run git status and summarize changes" + # Output: [sonnet - 8.23s - 1 tool calls - 142 tokens - \$0.0145] + # Other options ask --system "You are a pirate" "Tell me about sailing" ask -r "Tell me a joke" @@ -230,6 +236,9 @@ while [ $# -gt 0 ]; do --debug) DEBUG=true shift ;; + --stats) + SHOW_STATS=true + shift ;; --context) WITH_CONTEXT=true shift ;; @@ -371,15 +380,44 @@ else echo "$content" fi -# Show metadata -ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc)) -INPUT_TOKENS=$(echo "$response" | jq -r '.input_tokens // 0' 2>/dev/null) -OUTPUT_TOKENS=$(echo "$response" | jq -r '.output_tokens // 0' 2>/dev/null) +# Show metadata (only if --stats enabled) +if [ "$SHOW_STATS" = true ]; then + # ANSI color codes + GRAY='\033[90m' + BLUE='\033[94m' + GREEN='\033[92m' + YELLOW='\033[93m' + RESET='\033[0m' + + ELAPSED=$(printf "%.2f" $(echo "$(date +%s.%N) - $START_TIME" | bc)) + INPUT_TOKENS=$(echo "$response" | jq -r '.usage.input_tokens // .input_tokens // 0' 2>/dev/null) + OUTPUT_TOKENS=$(echo "$response" | jq -r '.usage.output_tokens // .output_tokens // 0' 2>/dev/null) + NUM_TURNS=$(echo "$response" | jq -r '.num_turns // 0' 2>/dev/null) + COST=$(echo "$response" | jq -r '.total_cost_usd // 0' 2>/dev/null) -echo -if [ "$INPUT_TOKENS" != "0" ] && [ "$OUTPUT_TOKENS" != "0" ]; then - TPS=$(echo "scale=1; $OUTPUT_TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0") - echo "[$MODEL - ${ELAPSED}s - ${OUTPUT_TOKENS} tokens - ${TPS} tok/s]" >&2 -else - echo "[$MODEL - ${ELAPSED}s]" >&2 + echo >&2 + # Build metadata string with colors + metadata="${GRAY}[${BLUE}${MODEL}${GRAY} - ${GREEN}${ELAPSED}s${RESET}" + + # Calculate tool calls (turns - 1) + if [ "$NUM_TURNS" != "0" ] && [ "$NUM_TURNS" != "null" ]; then + TOOL_CALLS=$((NUM_TURNS - 1)) + metadata="$metadata${GRAY} - ${YELLOW}${TOOL_CALLS} tool calls${RESET}" + fi + + # Add tokens and cost/tps if available + if [ "$OUTPUT_TOKENS" != "0" ] && [ "$OUTPUT_TOKENS" != "null" ]; then + metadata="$metadata${GRAY} - ${RESET}${OUTPUT_TOKENS} tokens" + + # Show cost if available (rounded to 4 decimal places), otherwise show tok/s + if [ "$COST" != "0" ] && [ "$COST" != "null" ]; then + ROUNDED_COST=$(printf "%.4f" "$COST") + metadata="$metadata${GRAY} - ${GREEN}\$${ROUNDED_COST}${RESET}" + else + TPS=$(echo "scale=1; $OUTPUT_TOKENS / $ELAPSED" | bc 2>/dev/null || echo "0.0") + metadata="$metadata${GRAY} - ${GREEN}${TPS} tok/s${RESET}" + fi + fi + + echo -e "$metadata${GRAY}]${RESET}" >&2 fi From 431d7c7c957f7d0384408f48a6fc011d6c822d9e Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Thu, 27 Nov 2025 10:18:10 -0600 Subject: [PATCH 5/6] dont need --- install.sh | 58 ------------------------------------------------------ 1 file changed, 58 deletions(-) delete mode 100755 install.sh diff --git a/install.sh b/install.sh deleted file mode 100755 index 7e40519..0000000 --- a/install.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env 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'" \ No newline at end of file From 22d955990a48185b53f597639967c5acc341a2b5 Mon Sep 17 00:00:00 2001 From: Eric Helgeson Date: Thu, 27 Nov 2025 10:19:32 -0600 Subject: [PATCH 6/6] ``` Add file editing capabilities and improve documentation - Enable file editing by default in current working directory - Add --no-edits flag to disable file editing when needed - Add --permission-mode and --allowed-tools flags for fine-grained control - Improve documentation with file editing examples - Add permission denial error handling with helpful messages - Add stdin warning when prompt is provided but --context not used - Update README with comprehensive file editing and git workflow examples --- README.md | 145 ++++++++++++++++++++++++++++++++++++------------------ ask | 119 ++++++++++++++++++++++++++++++++------------ 2 files changed, 184 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 76be733..a0efe50 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ask - AI CLI tool +# ask - Claude Code CLI wrapper -A lightweight bash script for querying AI models via the OpenRouter API, optimized for direct, executable output. +A lightweight bash script for querying Claude AI via the Claude Code CLI, optimized for direct, executable output with file editing capabilities. ## Quick start @@ -12,16 +12,15 @@ cd ask chmod +x ask sudo cp ask /usr/local/bin/ - -# Make sure you have you OpenRouter API key -export OPENROUTER_API_KEY="your-api-key-here" +# Install Claude Code CLI first +# Visit: https://claude.com/claude-code # Test it -> ask remove lines in file1 that appear in file2 +ask "Write a hello world in Python" -grep -vFf file2 file1 > file3 && mv file3 file1 +print("Hello, World!") -[inception/mercury-coder via Inception - 0.66s - 20.9 tok/s] +[sonnet - 1.23s - 0 tool calls - 12 tokens - $0.0001] ``` We also provide a handy install script. @@ -31,36 +30,24 @@ We also provide a handy install script. ### Basic usage ```bash -ask ffmpeg command to convert mp4 to gif +ask "ffmpeg command to convert mp4 to gif" ``` ### Model selection ```bash -# Default model (Mercury Coder - optimized for code) -ask find files larger than 20mb +# Default model (Claude Sonnet) +ask "find files larger than 20mb" # 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" -``` +ask -s "prompt" # Claude Sonnet (default, balanced) +ask -o "prompt" # Claude Opus (most capable, complex reasoning) +ask -h "prompt" # Claude Haiku (fastest, simple tasks) -### Provider routing - -Specify provider order for fallback support: - -```bash -ask --provider "cerebras,together" "Generate code" +# Custom model by name +ask -m haiku "What is 2+2?" ``` -This will try Cerebras first, then fall back to Together if needed. - ### System prompts ```bash @@ -71,36 +58,60 @@ ask --system "You are a pirate" "Tell me about sailing" ask -r "What is 2+2?" ``` -### Streaming mode +### File editing -Get responses as they're generated: +File editing is **enabled by default** in the current working directory: ```bash -ask --stream "Tell me a long story" +# Claude can read and edit files automatically +ask "Read README.md and update it with current features" +ask "Fix the bug in server.js" + +# Disable file editing if you only want analysis +ask --no-edits "Just analyze server.js, don't modify it" +``` + +### Running commands + +Claude can run bash commands to gather context: + +```bash +# Claude will run git commands for you +ask "Look at git diff and write a commit message" +ask "Run git status and summarize changes" + +# Show statistics to see tool usage +ask --stats "Run git status and summarize changes" ``` ### Pipe input ```bash +# Pipe as prompt echo "Fix this code: print('hello world)" | ask -cat script.py | ask "Review this code" + +# Pipe as context with --context flag +git diff | ask --context "Write a commit message for this diff" +cat error.log | ask --context "Explain what caused this error" ``` ## 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 | +| `-s` | Use Claude Sonnet (default) | +| `-o` | Use Claude Opus | +| `-h` | Use Claude Haiku | +| `-m MODEL` | Use custom Claude model | +| `-r` | Disable system prompt (raw model behavior) | +| `--system TEXT` | Set custom system prompt | +| `--context` | Pipe stdin as context, combine with prompt argument | +| `--no-edits` | Disable file editing (enabled by default) | +| `--permission-mode MODE` | Set permission mode (e.g., acceptEdits) | +| `--allowed-tools TOOLS` | Comma-separated list of allowed tools | +| `--stats` | Show statistics (time, tool calls, tokens, cost) | +| `--debug` | Show system context, prompts, and command (stderr) | +| `--help` | Show help message | ## Common use cases @@ -123,6 +134,29 @@ ask "Python function to calculate factorial" cat script.py | ask "Find potential bugs in this code" ``` +### File editing and code modification +```bash +# Claude can read and modify files directly +ask "Read server.js and add error handling to the API routes" +ask "Fix the typo in README.md line 42" +ask "Refactor utils.py to use type hints" + +# Disable editing for read-only analysis +ask --no-edits "Analyze the performance of this algorithm in sort.js" +``` + +### Git workflow +```bash +# Claude can run git commands and analyze changes +ask "Look at git diff and write a commit message" +ask "Run git status and tell me what needs attention" +ask "Review the last 3 commits and summarize changes" + +# See what Claude is doing with --stats +ask --stats "Run git log --oneline -5 and summarize the recent changes" +# Output: [sonnet - 2.15s - 1 tool calls - 89 tokens - $0.0023] +``` + ### Quick answers ```bash # Calculations @@ -142,19 +176,34 @@ ask "List all Python files" | ask "Generate a script to check syntax of these fi # Use with other tools docker ps -a | ask "Which containers are using the most memory?" +# Debug mode to see what's happening +ask --debug "Why is my build failing?" ``` ## 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` +- Claude Code CLI - The underlying AI engine + +### Installation +1. Install Claude Code CLI from [claude.com/claude-code](https://claude.com/claude-code) +2. Clone this repository +3. Make the script executable: `chmod +x ask` +4. Copy to your PATH: `sudo cp ask /usr/local/bin/` + +## Features + +- **Direct executable output** - Optimized for command generation and piping +- **File editing** - Claude can read and modify files in your working directory +- **Command execution** - Claude can run bash commands to gather context +- **Smart system context** - Automatically detects OS, tools, git repos, and project type +- **Model selection** - Choose between Sonnet, Opus, or Haiku +- **Statistics** - Track tool usage, tokens, and costs with `--stats` +- **Debug mode** - See exactly what prompts are being sent +- **Caching** - System context is cached for 24 hours for performance ## License diff --git a/ask b/ask index 8b664dc..b96c278 100755 --- a/ask +++ b/ask @@ -34,6 +34,9 @@ NO_SYSTEM=false DEBUG=false WITH_CONTEXT=false SHOW_STATS=false +ALLOW_EDITS=true # Enable file edits by default +PERMISSION_MODE="" +ALLOWED_TOOLS="" # Default system prompt (direct answers) DEFAULT_PROMPT="You are a direct answer engine. Output ONLY the requested information. @@ -58,16 +61,21 @@ ask - Query Claude AI via Claude Code CLI Usage: ask [OPTIONS] [PROMPT] Options: - -s Use Claude Sonnet (default) - -o Use Claude Opus - -h Use Claude Haiku - -m MODEL Use custom Claude model - -r Disable system prompt (raw model behavior) - --system Set custom system prompt for the conversation - --context Pipe stdin as context, combine with prompt argument - --stats Show statistics (time, tool calls, tokens, cost) - --debug Show system context, prompts, and command (output to stderr) - --help Show this help message + -s Use Claude Sonnet (default) + -o Use Claude Opus + -h Use Claude Haiku + -m MODEL Use custom Claude model + -r Disable system prompt (raw model behavior) + --system TEXT Set custom system prompt for the conversation + --context Pipe stdin as context, combine with prompt argument + --no-edits Disable file editing (enabled by default) + --permission-mode MODE Set permission mode (e.g., acceptEdits) + --allowed-tools TOOLS Comma-separated list of allowed tools + --stats Show statistics (time, tool calls, tokens, cost) + --debug Show system context, prompts, and command (stderr) + --help Show this help message + +Note: File editing is enabled by default in the current working directory. Examples: # Basic usage @@ -82,6 +90,11 @@ Examples: ask "Look at git diff and write a commit message" ask "Run git status and summarize changes" + # File editing (enabled by default) + ask "Read README.md and update it with current features" + ask "Fix the bug in server.js" + ask --no-edits "Just analyze server.js, don't modify it" + # Pipe context with --context flag (alternative) git diff | ask --context "Write a commit message for this diff" cat error.log | ask --context "Explain what caused this error" @@ -242,6 +255,18 @@ while [ $# -gt 0 ]; do --context) WITH_CONTEXT=true shift ;; + --allow-edits) + ALLOW_EDITS=true + shift ;; + --no-edits) + ALLOW_EDITS=false + shift ;; + --permission-mode) + PERMISSION_MODE="${2:?Error: --permission-mode requires a value}" + shift 2 ;; + --allowed-tools) + ALLOWED_TOOLS="${2:?Error: --allowed-tools requires a value}" + shift 2 ;; -[soh]) MODEL="$(get_model "${1:1}")" shift ;; @@ -287,6 +312,13 @@ elif [ -z "$PROMPT" ]; then fi # Read all stdin, preserving multi-line format PROMPT=$(cat) +else + # Prompt provided: warn if stdin is being ignored + if [ ! -t 0 ]; then + echo "Warning: stdin detected but will be ignored. Use --context to include it." >&2 + # Consume stdin to prevent it from being passed to claude + cat > /dev/null + fi fi # Gather system context (unless in raw mode) @@ -310,6 +342,23 @@ fi # Record start time START_TIME=$(date +%s.%N) +# Build Claude CLI command with optional flags +CLAUDE_ARGS="-p \"$PROMPT\" --model \"$MODEL\"" + +# Add system prompt if provided +[ -n "$SYSTEM_PROMPT" ] && CLAUDE_ARGS="$CLAUDE_ARGS --system-prompt \"$SYSTEM_PROMPT\"" + +# Add permission flags +if [ "$ALLOW_EDITS" = true ]; then + CLAUDE_ARGS="$CLAUDE_ARGS --permission-mode acceptEdits" +fi + +[ -n "$PERMISSION_MODE" ] && CLAUDE_ARGS="$CLAUDE_ARGS --permission-mode \"$PERMISSION_MODE\"" +[ -n "$ALLOWED_TOOLS" ] && CLAUDE_ARGS="$CLAUDE_ARGS --allowedTools \"$ALLOWED_TOOLS\"" + +# Always request JSON output +CLAUDE_ARGS="$CLAUDE_ARGS --output-format json" + # Debug output if [ "$DEBUG" = true ]; then echo "=== SYSTEM CONTEXT ===" >&2 @@ -332,28 +381,16 @@ if [ "$DEBUG" = true ]; then echo "$PROMPT" >&2 echo >&2 - # Build debug command string - debug_cmd="claude -p \"\" --model \"$MODEL\"" - [ -n "$SYSTEM_PROMPT" ] && debug_cmd="$debug_cmd --system-prompt \"\"" - debug_cmd="$debug_cmd --output-format json" - echo "=== CLAUDE COMMAND ===" >&2 - echo "$debug_cmd" >&2 + echo "claude $CLAUDE_ARGS" | sed 's/"[^"]*"/"<...>"/g' >&2 echo >&2 fi # Add newline before answer echo -# Execute Claude CLI with proper argument passing -if [ -n "$SYSTEM_PROMPT" ]; then - # With custom system prompt - response=$(claude -p "$PROMPT" --model "$MODEL" --system-prompt "$SYSTEM_PROMPT" --output-format json 2>&1) -else - # Without system prompt (raw mode) - response=$(claude -p "$PROMPT" --model "$MODEL" --output-format json 2>&1) -fi - +# Execute Claude CLI +response=$(eval claude $CLAUDE_ARGS 2>&1) exit_code=$? # Debug output for response @@ -371,15 +408,33 @@ if [ $exit_code -ne 0 ]; then fi # Extract and print content from JSON response -content=$(echo "$response" | jq -r '.result // .content // .response // .' 2>/dev/null) - -# If jq parsing failed or returned null, just print the raw response -if [ -z "$content" ] || [ "$content" = "null" ]; then - echo "$response" -else - echo "$content" +content=$(echo "$response" | jq -r '.result // empty' 2>/dev/null) + +# Check if result is empty or if there were permission denials +if [ -z "$content" ]; then + # Check for permission denials + denials=$(echo "$response" | jq -r '.permission_denials // [] | length' 2>/dev/null) + if [ "$denials" != "0" ] && [ "$denials" != "null" ]; then + echo "Error: Claude tried to perform an action but was denied permission." >&2 + echo "" >&2 + echo "Denied actions:" >&2 + echo "$response" | jq -r '.permission_denials[] | " - \(.tool_name)"' 2>/dev/null >&2 + echo "" >&2 + echo "This may happen if:" >&2 + echo " 1. File is outside current working directory" >&2 + echo " 2. Insufficient file system permissions" >&2 + echo " 3. Run: claude (interactive mode) for more control" >&2 + exit 1 + else + # No content and no denials - unexpected + echo "Error: No response content received" >&2 + [ "$DEBUG" = true ] && echo "$response" >&2 + exit 1 + fi fi +echo "$content" + # Show metadata (only if --stats enabled) if [ "$SHOW_STATS" = true ]; then # ANSI color codes