mirror of
https://github.com/zsh-users/zsh-autosuggestions.git
synced 2026-02-16 16:42:29 +01:00
Add new AI strategy that queries OpenAI-compatible LLM APIs to generate intelligent command completions based on partial input, working directory, and recent shell history. - Add AI strategy implementation with JSON escaping - Add context gathering with PWD prioritization - Add response normalization for clean suggestions - Add configuration defaults for AI settings - Add comprehensive test suite with mocked responses - Update README with setup guide and examples Enables LLM-powered completions via ZSH_AUTOSUGGEST_AI_API_KEY with silent failure and fallback to next strategy. Supports OpenAI, Ollama, and custom endpoints. Requires curl and jq. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
192 lines
5.3 KiB
Bash
192 lines
5.3 KiB
Bash
|
|
#--------------------------------------------------------------------#
|
|
# AI Suggestion Strategy #
|
|
#--------------------------------------------------------------------#
|
|
# Queries an OpenAI-compatible LLM API to generate command
|
|
# completions based on partial input, working directory, and
|
|
# recent shell history.
|
|
#
|
|
|
|
_zsh_autosuggest_strategy_ai_json_escape() {
|
|
# Reset options to defaults and enable LOCAL_OPTIONS
|
|
emulate -L zsh
|
|
|
|
local input="$1"
|
|
local output=""
|
|
local char
|
|
|
|
for ((i=1; i<=${#input}; i++)); do
|
|
char="${input:$((i-1)):1}"
|
|
case "$char" in
|
|
'\\') output+='\\\\' ;;
|
|
'"') output+='\"' ;;
|
|
$'\n') output+='\n' ;;
|
|
$'\t') output+='\t' ;;
|
|
$'\r') output+='\r' ;;
|
|
[[:cntrl:]]) ;; # Skip other control chars
|
|
*) output+="$char" ;;
|
|
esac
|
|
done
|
|
|
|
printf '%s' "$output"
|
|
}
|
|
|
|
_zsh_autosuggest_strategy_ai_gather_context() {
|
|
# Reset options to defaults and enable LOCAL_OPTIONS
|
|
emulate -L zsh
|
|
|
|
local max_lines="${ZSH_AUTOSUGGEST_AI_HISTORY_LINES:-20}"
|
|
local prefer_pwd="${ZSH_AUTOSUGGEST_AI_PREFER_PWD_HISTORY:-yes}"
|
|
local pwd_basename="${PWD:t}"
|
|
local -a context_lines
|
|
local -a pwd_lines
|
|
local -a other_lines
|
|
local line
|
|
|
|
# Iterate from most recent history
|
|
for line in "${(@On)history}"; do
|
|
# Truncate long lines
|
|
if [[ ${#line} -gt 200 ]]; then
|
|
line="${line:0:200}..."
|
|
fi
|
|
|
|
# Categorize by PWD relevance
|
|
if [[ "$prefer_pwd" == "yes" ]] && [[ "$line" == *"$pwd_basename"* || "$line" == *"./"* || "$line" == *"../"* ]]; then
|
|
pwd_lines+=("$line")
|
|
else
|
|
other_lines+=("$line")
|
|
fi
|
|
done
|
|
|
|
# Prioritize PWD-relevant lines, then fill with others
|
|
context_lines=("${pwd_lines[@]}" "${other_lines[@]}")
|
|
context_lines=("${(@)context_lines[1,$max_lines]}")
|
|
|
|
# Return via reply array
|
|
reply=("${context_lines[@]}")
|
|
}
|
|
|
|
_zsh_autosuggest_strategy_ai_normalize() {
|
|
# Reset options to defaults and enable LOCAL_OPTIONS
|
|
emulate -L zsh
|
|
|
|
local response="$1"
|
|
local buffer="$2"
|
|
local result=""
|
|
|
|
# Strip \r
|
|
response="${response//$'\r'/}"
|
|
|
|
# Strip markdown code fences
|
|
response="${response##\`\`\`*$'\n'}"
|
|
response="${response%%$'\n'\`\`\`}"
|
|
|
|
# Strip surrounding quotes
|
|
if [[ "$response" == \"*\" || "$response" == \'*\' ]]; then
|
|
response="${response:1:-1}"
|
|
fi
|
|
|
|
# Trim whitespace
|
|
response="${response##[[:space:]]##}"
|
|
response="${response%%[[:space:]]##}"
|
|
|
|
# Take first line only
|
|
result="${response%%$'\n'*}"
|
|
|
|
# If response starts with buffer, extract suffix
|
|
if [[ "$result" == "$buffer"* ]]; then
|
|
local suffix="${result#$buffer}"
|
|
result="$buffer$suffix"
|
|
# If response looks like a pure suffix, prepend buffer
|
|
elif [[ -n "$buffer" ]] && [[ "$result" != "$buffer"* ]] && [[ "${buffer}${result}" == [[:print:]]* ]]; then
|
|
result="$buffer$result"
|
|
fi
|
|
|
|
printf '%s' "$result"
|
|
}
|
|
|
|
_zsh_autosuggest_strategy_ai() {
|
|
# Reset options to defaults and enable LOCAL_OPTIONS
|
|
emulate -L zsh
|
|
|
|
typeset -g suggestion
|
|
local buffer="$1"
|
|
|
|
# Early return if API key not set (opt-in gate)
|
|
[[ -z "$ZSH_AUTOSUGGEST_AI_API_KEY" ]] && return
|
|
|
|
# Early return if curl or jq not available
|
|
[[ -z "${commands[curl]}" ]] || [[ -z "${commands[jq]}" ]] && return
|
|
|
|
# Early return if input too short
|
|
local min_input="${ZSH_AUTOSUGGEST_AI_MIN_INPUT:-3}"
|
|
[[ ${#buffer} -lt $min_input ]] && return
|
|
|
|
# Gather context
|
|
local -a context
|
|
_zsh_autosuggest_strategy_ai_gather_context
|
|
context=("${reply[@]}")
|
|
|
|
# Build context string
|
|
local context_str=""
|
|
for line in "${context[@]}"; do
|
|
if [[ -n "$context_str" ]]; then
|
|
context_str+=", "
|
|
fi
|
|
context_str+="\"$(_zsh_autosuggest_strategy_ai_json_escape "$line")\""
|
|
done
|
|
|
|
# Build JSON request body
|
|
local system_prompt="You are a shell command auto-completion engine. Given the user's partial command, working directory, and recent history, predict the complete command. Reply ONLY with the complete command. No explanations, no markdown, no quotes."
|
|
local user_message="Working directory: $PWD\nRecent history: [$context_str]\nPartial command: $buffer"
|
|
|
|
local json_body
|
|
json_body=$(printf '{
|
|
"model": "%s",
|
|
"messages": [
|
|
{"role": "system", "content": "%s"},
|
|
{"role": "user", "content": "%s"}
|
|
],
|
|
"temperature": 0.3,
|
|
"max_tokens": 100
|
|
}' \
|
|
"${ZSH_AUTOSUGGEST_AI_MODEL:-gpt-3.5-turbo}" \
|
|
"$(_zsh_autosuggest_strategy_ai_json_escape "$system_prompt")" \
|
|
"$(_zsh_autosuggest_strategy_ai_json_escape "$user_message")")
|
|
|
|
# Make API request
|
|
local endpoint="${ZSH_AUTOSUGGEST_AI_ENDPOINT:-https://api.openai.com/v1/chat/completions}"
|
|
local timeout="${ZSH_AUTOSUGGEST_AI_TIMEOUT:-5}"
|
|
local response
|
|
|
|
response=$(curl --silent --max-time "$timeout" \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $ZSH_AUTOSUGGEST_AI_API_KEY" \
|
|
-d "$json_body" \
|
|
-w '\n%{http_code}' \
|
|
"$endpoint" 2>/dev/null)
|
|
|
|
# Check curl exit status
|
|
[[ $? -ne 0 ]] && return
|
|
|
|
# Split response body from HTTP status
|
|
local http_code="${response##*$'\n'}"
|
|
local body="${response%$'\n'*}"
|
|
|
|
# Early return on non-2xx status
|
|
[[ "$http_code" != 2* ]] && return
|
|
|
|
# Extract content from JSON response
|
|
local content
|
|
content=$(printf '%s' "$body" | jq -r '.choices[0].message.content // empty' 2>/dev/null)
|
|
|
|
# Early return if extraction failed
|
|
[[ -z "$content" ]] && return
|
|
|
|
# Normalize response
|
|
local normalized
|
|
normalized="$(_zsh_autosuggest_strategy_ai_normalize "$content" "$buffer")"
|
|
|
|
# Set suggestion
|
|
[[ -n "$normalized" ]] && suggestion="$normalized"
|
|
}
|