diff --git a/README.md b/README.md index b897f68..0953d5b 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,29 @@ super-man ask "find large files" # ask a question super-man task "monitor cpu usage" # describe what you want to do super-man category files # browse by category (files/text/network/system) super-man explain "tar -czf" # explain what a command does -super-man ai "count lines in js files" # AI-powered NL2SH model (advanced) +super-man ai "count lines in js files" # AI bash one-liner via local LLM (advanced) ``` +### AI mode (local LLM via ollama) + +`ai` generates a bash one-liner from plain English using a local [ollama](https://ollama.com) +model. The model is configurable, so you can A/B different local LLMs: + +```bash +super-man ai "find python files modified last week" # default model +super-man ai --model qwen2.5:3b "count lines in all js files" # override model +super-man ai --compare "list the 5 largest files" # run several models side by side +``` + +| Env var | Purpose | Default | +|---------|---------|---------| +| `SUPERMAN_AI_MODEL` | Default model for `ai` | `qwen2.5-coder:1.5b` | +| `SUPERMAN_AI_COMPARE_MODELS` | Space-separated list for `--compare` | `qwen2.5-coder:1.5b westenfelder/NL2SH` | + +General coder models (e.g. `qwen2.5-coder`) are prompt-wrapped to emit a single bare +command; the purpose-built `westenfelder/NL2SH` model receives the raw query. Pull +either with `ollama pull `. + `Esc` in the flag browser returns to the command list; `Esc` on the home screen exits. diff --git a/super-man.sh b/super-man.sh index 2f08324..f604352 100755 --- a/super-man.sh +++ b/super-man.sh @@ -41,7 +41,7 @@ show_help() { echo -e " ${CYAN}Natural Language Queries:${NC}" echo -e " ${GREEN}ask${NC} \"query\" Ask in natural language (database)" echo -e " ${GREEN}task${NC} \"description\" Describe what you want to do (database)" - echo -e " ${GREEN}ai${NC} \"complex query\" AI-powered NL2SH model (advanced) 🤖" + echo -e " ${GREEN}ai${NC} \"complex query\" AI bash one-liner via local LLM (advanced) 🤖" echo "" echo -e " ${CYAN}Browse & Explore:${NC}" echo -e " ${GREEN}category${NC} NAME Browse by category (files/text/network/system)" @@ -77,15 +77,18 @@ show_help() { echo -e " ${GREEN}✓${NC} fzf (for interactive mode only)" echo "" echo -e "${BOLD}${YELLOW}AI MODE${NC} 🤖" - echo -e " ${GREEN}✓ NL2SH model detected!${NC} Use AI for complex queries:" + echo -e " Generate bash one-liners from plain English via a local ollama model:" echo -e " ${GREEN}super-man ai \"find python files modified last week\"${NC}" - echo -e " ${GREEN}super-man ai \"count lines in all js files\"${NC}" + echo -e " ${GREEN}super-man ai --model qwen2.5:3b \"count lines in all js files\"${NC}" + echo -e " ${GREEN}super-man ai --compare \"list the 5 largest files\"${NC} # A/B models" echo "" - echo " Other models available: llama3.2, qwen2.5" - echo " To switch models, edit mode_ai() function" + echo -e " Default model: ${CYAN}${SUPERMAN_AI_MODEL}${NC}" + echo " Configure via env vars:" + echo " SUPERMAN_AI_MODEL default model (e.g. westenfelder/NL2SH)" + echo " SUPERMAN_AI_COMPARE_MODELS space-separated list for --compare" echo "" echo -e "${YELLOW}Database:${NC} $DB_FILE" - echo -e "${YELLOW}Version:${NC} 2.1.0 (250+ commands, AI-powered)" + echo -e "${YELLOW}Version:${NC} 2.2.0 (250+ commands, configurable local AI)" echo "" } @@ -308,15 +311,85 @@ mode_category() { } # AI mode - use NL2SH model for complex queries +# Generate bash one-liners from natural language via a local ollama model. +# The model is configurable so we can A/B different local LLMs: +# SUPERMAN_AI_MODEL default model (default: qwen2.5-coder:1.5b) +# SUPERMAN_AI_COMPARE_MODELS space-separated list used by `ai --compare` +# Override per call with `--model NAME`; run every comparison model side by +# side with `--compare`. NL2SH is purpose-built for NL->shell so it receives +# the raw query; general coder models get an instruction wrapper plus markdown +# fence stripping so they too emit a single bare command. +SUPERMAN_AI_MODEL="${SUPERMAN_AI_MODEL:-qwen2.5-coder:1.5b}" +SUPERMAN_AI_COMPARE_MODELS="${SUPERMAN_AI_COMPARE_MODELS:-qwen2.5-coder:1.5b westenfelder/NL2SH}" + +# Build a model-appropriate prompt for a query. +ai_build_prompt() { + local model="$1" query="$2" + case "$model" in + *NL2SH*|*nl2sh*) + printf '%s' "$query" + ;; + *) + printf 'You are a bash expert. Output ONLY a single bash one-liner that accomplishes the task. No explanation, no comments, no markdown code fences. Task: %s' "$query" + ;; + esac +} + +# Strip markdown fences and surrounding blank lines from a raw model response. +ai_clean_response() { + awk '/^[[:space:]]*```/ { next } { print }' \ + | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' \ + | sed '/^$/d' +} + +# True if an ollama model is pulled locally. +ai_model_installed() { + ollama list 2>/dev/null | grep -q "$1" +} + +# Generate a cleaned command from one model for a query. +ai_generate() { + local model="$1" query="$2" + ollama run "$model" "$(ai_build_prompt "$model" "$query")" 2>/dev/null | ai_clean_response +} + +# Print the man/help NAME line for the base command of a generated one-liner. +ai_explain_base() { + local base_cmd + base_cmd=$(echo "$1" | awk '{print $1}') + if type "$base_cmd" 2>/dev/null | grep -q "shell builtin"; then + echo -e "${YELLOW}Command Info:${NC}" + help "$base_cmd" 2>/dev/null | head -3 + echo "" + elif command -v "$base_cmd" &> /dev/null; then + echo -e "${YELLOW}Command Info:${NC}" + man "$base_cmd" 2>/dev/null | col -b | grep -A 2 "^NAME" | tail -2 + echo "" + fi +} + mode_ai() { - local user_query="$1" + local compare=false + local model="$SUPERMAN_AI_MODEL" + local args=() + while [ $# -gt 0 ]; do + case "$1" in + --compare|-c) compare=true ;; + --model|-m) shift; model="$1" ;; + *) args+=("$1") ;; + esac + shift + done + local user_query="${args[*]}" if [ -z "$user_query" ]; then echo -e "${RED}Error: No query provided${NC}" - echo "Usage: super-man ai \"your complex query\"" + echo "Usage: super-man ai [--model NAME|--compare] \"your query\"" echo "" - echo "Example:" + echo "Examples:" echo " super-man ai \"find all python files modified in last week\"" + echo " super-man ai --model qwen2.5:3b \"count lines in all js files\"" + echo " super-man ai --compare \"list the 5 largest files\"" return 1 fi @@ -331,18 +404,50 @@ mode_ai() { return 1 fi - # Check if NL2SH model is available - if ! ollama list | grep -q "westenfelder/NL2SH"; then - echo -e "${YELLOW}NL2SH model not found. Available models:${NC}" + # ── Compare mode: run every configured model side by side ────────────────── + if $compare; then + echo "" + echo -e "${CYAN}🤖 AI Compare${NC}" + echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" + echo -e "${YELLOW}Query:${NC} $user_query" + echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" + local m + for m in $SUPERMAN_AI_COMPARE_MODELS; do + echo "" + echo -e "${BOLD}${MAGENTA}▸ $m${NC}" + if ! ai_model_installed "$m"; then + echo -e " ${DIM}not installed — pull with: ollama pull $m${NC}" + continue + fi + local start end elapsed out + start=$(date +%s.%N) + out=$(ai_generate "$m" "$user_query") + end=$(date +%s.%N) + elapsed=$(awk "BEGIN{printf \"%.1f\", $end-$start}") + if [ -z "$out" ]; then + echo -e " ${RED}(no response)${NC} ${DIM}(${elapsed}s)${NC}" + else + echo -e " ${GREEN}$out${NC} ${DIM}(${elapsed}s)${NC}" + fi + done + echo "" + echo -e "${DIM}Note: Always review AI-generated commands before running them!${NC}" + echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" + return 0 + fi + + # ── Single-model mode ────────────────────────────────────────────────────── + if ! ai_model_installed "$model"; then + echo -e "${YELLOW}Model '$model' not found. Installed models:${NC}" ollama list echo "" - echo "Pull NL2SH model with:" - echo " ollama pull westenfelder/NL2SH" + echo "Pull it with: ollama pull $model" + echo "Or pick another: super-man ai --model NAME \"$user_query\"" return 1 fi echo "" - echo -e "${CYAN}🤖 AI Assistant - Using NL2SH Model${NC}" + echo -e "${CYAN}🤖 AI Assistant — $model${NC}" echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" echo -e "${YELLOW}Query:${NC} $user_query" echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}" @@ -350,8 +455,7 @@ mode_ai() { echo -e "${DIM}Generating bash command...${NC}" echo "" - # Query the NL2SH model - local response=$(ollama run westenfelder/NL2SH "$user_query" 2>/dev/null) + local response=$(ai_generate "$model" "$user_query") if [ -z "$response" ]; then echo -e "${RED}Error: No response from AI model${NC}" @@ -365,19 +469,7 @@ mode_ai() { echo -e " ${MAGENTA}$response${NC}" echo "" - # Extract the base command for explanation - local base_cmd=$(echo "$response" | awk '{print $1}') - - # Try to provide explanation - if command -v "$base_cmd" &> /dev/null || type "$base_cmd" 2>/dev/null | grep -q "shell builtin"; then - echo -e "${YELLOW}Command Info:${NC}" - if type "$base_cmd" 2>/dev/null | grep -q "shell builtin"; then - help "$base_cmd" 2>/dev/null | head -3 - else - man "$base_cmd" 2>/dev/null | col -b | grep -A 2 "^NAME" | tail -2 - fi - echo "" - fi + ai_explain_base "$response" echo -e "${DIM}Note: Always review AI-generated commands before running them!${NC}" echo "" @@ -1250,8 +1342,9 @@ else QUERY="$2" ;; ai) - MODE="ai" - QUERY="$2" + shift + mode_ai "$@" + exit $? ;; -*) echo "Unknown option: $1" @@ -1280,10 +1373,6 @@ if [ -n "$MODE" ]; then mode_category "$QUERY" exit $? ;; - ai) - mode_ai "$QUERY" - exit $? - ;; esac fi