first commit
This commit is contained in:
960
finish.sh
Normal file
960
finish.sh
Normal file
@@ -0,0 +1,960 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# Enhanced Error Handling #
|
||||
###############################################################################
|
||||
|
||||
error_exit() {
|
||||
echo -e "\e[finish.sh - $1\e[0m" >&2
|
||||
# In a completion context, exit is too severe. Use return instead.
|
||||
return 1
|
||||
}
|
||||
|
||||
echo_error() {
|
||||
echo -e "\e[finish.sh - $1\e[0m" >&2
|
||||
}
|
||||
|
||||
echo_green() {
|
||||
echo -e "\e[32m$1\e[0m"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Global Variables & Model Definitions #
|
||||
###############################################################################
|
||||
|
||||
export ACSH_VERSION=0.5.0
|
||||
|
||||
unset _finishte_modellist
|
||||
declare -A _finishte_modellist
|
||||
# LM-Studio models
|
||||
_finishte_modellist['lmstudio: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:1234/v1/chat/completions", "model": "darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2", "provider": "lmstudio" }'
|
||||
# Ollama model
|
||||
_finishte_modellist['ollama: codellama']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:11434/api/chat", "model": "codellama", "provider": "ollama" }'
|
||||
|
||||
###############################################################################
|
||||
# System Information Functions #
|
||||
###############################################################################
|
||||
|
||||
_get_terminal_info() {
|
||||
local terminal_info=" * User name: \$USER=$USER
|
||||
* Current directory: \$PWD=$PWD
|
||||
* Previous directory: \$OLDPWD=$OLDPWD
|
||||
* Home directory: \$HOME=$HOME
|
||||
* Operating system: \$OSTYPE=$OSTYPE
|
||||
* Shell: \$BASH=$BASH
|
||||
* Terminal type: \$TERM=$TERM
|
||||
* Hostname: \$HOSTNAME"
|
||||
echo "$terminal_info"
|
||||
}
|
||||
|
||||
machine_signature() {
|
||||
local signature
|
||||
signature=$(echo "$(uname -a)|$$USER" | md5sum | cut -d ' ' -f 1)
|
||||
echo "$signature"
|
||||
}
|
||||
|
||||
_system_info() {
|
||||
echo "# System Information"
|
||||
echo
|
||||
uname -a
|
||||
echo "SIGNATURE: $(machine_signature)"
|
||||
echo
|
||||
echo "BASH_VERSION: $BASH_VERSION"
|
||||
echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}"
|
||||
echo
|
||||
echo "## Terminal Information"
|
||||
_get_terminal_info
|
||||
}
|
||||
|
||||
_completion_vars() {
|
||||
echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}"
|
||||
echo "COMP_CWORD: ${COMP_CWORD}"
|
||||
echo "COMP_KEY: ${COMP_KEY}"
|
||||
echo "COMP_LINE: ${COMP_LINE}"
|
||||
echo "COMP_POINT: ${COMP_POINT}"
|
||||
echo "COMP_TYPE: ${COMP_TYPE}"
|
||||
echo "COMP_WORDBREAKS: ${COMP_WORDBREAKS}"
|
||||
echo "COMP_WORDS: ${COMP_WORDS[*]}"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# LLM Completion Functions #
|
||||
###############################################################################
|
||||
|
||||
_get_system_message_prompt() {
|
||||
echo "You are a helpful bash_completion script. Generate relevant and concise auto-complete suggestions for the given user command in the context of the current directory, operating system, command history, and environment variables. The output must be a list of two to five possible completions or rewritten commands, each on a new line, without spanning multiple lines. Each must be a valid command or chain of commands. Do not include backticks or quotes."
|
||||
}
|
||||
|
||||
_get_output_instructions() {
|
||||
echo "Provide a list of suggested completions or commands that could be run in the terminal. YOU MUST provide a list of two to five possible completions or rewritten commands. DO NOT wrap the commands in backticks or quotes. Each must be a valid command or chain of commands. Focus on the user's intent, recent commands, and the current environment. RETURN A JSON OBJECT WITH THE COMPLETIONS."
|
||||
}
|
||||
|
||||
_get_command_history() {
|
||||
local HISTORY_LIMIT=${ACSH_MAX_HISTORY_COMMANDS:-20}
|
||||
history | tail -n "$HISTORY_LIMIT"
|
||||
}
|
||||
|
||||
# Refined sanitization: only replace long hex sequences, UUIDs, and API-key–like tokens.
|
||||
_get_clean_command_history() {
|
||||
local recent_history
|
||||
recent_history=$(_get_command_history)
|
||||
recent_history=$(echo "$recent_history" | sed -E 's/\b[[:xdigit:]]{32,40}\b/REDACTED_HASH/g')
|
||||
recent_history=$(echo "$recent_history" | sed -E 's/\b[0-9a-fA-F-]{36}\b/REDACTED_UUID/g')
|
||||
recent_history=$(echo "$recent_history" | sed -E 's/\b[A-Za-z0-9]{16,40}\b/REDACTED_APIKEY/g')
|
||||
echo "$recent_history"
|
||||
}
|
||||
|
||||
_get_recent_files() {
|
||||
local FILE_LIMIT=${ACSH_MAX_RECENT_FILES:-20}
|
||||
find . -maxdepth 1 -type f -exec ls -ld {} + | sort -r | head -n "$FILE_LIMIT"
|
||||
}
|
||||
|
||||
# Rewritten _get_help_message using a heredoc to preserve formatting.
|
||||
_get_help_message() {
|
||||
local COMMAND HELP_INFO
|
||||
COMMAND=$(echo "$1" | awk '{print $1}')
|
||||
HELP_INFO=""
|
||||
{
|
||||
set +e
|
||||
HELP_INFO=$(cat <<EOF
|
||||
$($COMMAND --help 2>&1 || true)
|
||||
EOF
|
||||
)
|
||||
set -e
|
||||
} || HELP_INFO="'$COMMAND --help' not available"
|
||||
echo "$HELP_INFO"
|
||||
}
|
||||
|
||||
_build_prompt() {
|
||||
local user_input command_history terminal_context help_message recent_files output_instructions other_environment_variables prompt
|
||||
user_input="$*"
|
||||
command_history=$(_get_clean_command_history)
|
||||
terminal_context=$(_get_terminal_info)
|
||||
help_message=$(_get_help_message "$user_input")
|
||||
recent_files=$(_get_recent_files)
|
||||
output_instructions=$(_get_output_instructions)
|
||||
other_environment_variables=$(env | grep '=' | grep -v 'ACSH_' | awk -F= '{print $1}' | grep -v 'PWD\|OSTYPE\|BASH\|USER\|HOME\|TERM\|OLDPWD\|HOSTNAME')
|
||||
|
||||
prompt="User command: \`$user_input\`
|
||||
|
||||
# Terminal Context
|
||||
## Environment variables
|
||||
$terminal_context
|
||||
|
||||
Other defined environment variables
|
||||
\`\`\`
|
||||
$other_environment_variables
|
||||
\`\`\`
|
||||
|
||||
## History
|
||||
Recently run commands (some information redacted):
|
||||
\`\`\`
|
||||
$command_history
|
||||
\`\`\`
|
||||
|
||||
## File system
|
||||
Most recently modified files:
|
||||
\`\`\`
|
||||
$recent_files
|
||||
\`\`\`
|
||||
|
||||
## Help Information
|
||||
$help_message
|
||||
|
||||
# Instructions
|
||||
$output_instructions
|
||||
"
|
||||
echo "$prompt"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Payload Building Functions #
|
||||
###############################################################################
|
||||
|
||||
build_common_payload() {
|
||||
jq -n --arg model "$model" \
|
||||
--arg temperature "$temperature" \
|
||||
--arg system_prompt "$system_prompt" \
|
||||
--arg prompt_content "$prompt_content" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{role: "system", content: $system_prompt},
|
||||
{role: "user", content: $prompt_content}
|
||||
],
|
||||
temperature: ($temperature | tonumber)
|
||||
}'
|
||||
}
|
||||
|
||||
_build_payload() {
|
||||
local user_input prompt system_message_prompt payload acsh_prompt
|
||||
local model temperature
|
||||
model="${ACSH_MODEL:-gpt-4o}"
|
||||
temperature="${ACSH_TEMPERATURE:-0.0}"
|
||||
|
||||
user_input="$1"
|
||||
prompt=$(_build_prompt "$@")
|
||||
system_message_prompt=$(_get_system_message_prompt)
|
||||
|
||||
acsh_prompt="# SYSTEM PROMPT
|
||||
$system_message_prompt
|
||||
# USER MESSAGE
|
||||
$prompt"
|
||||
export ACSH_PROMPT="$acsh_prompt"
|
||||
|
||||
prompt_content="$prompt"
|
||||
system_prompt="$system_message_prompt"
|
||||
|
||||
local base_payload
|
||||
base_payload=$(build_common_payload)
|
||||
|
||||
case "${ACSH_PROVIDER^^}" in
|
||||
"OLLAMA")
|
||||
payload=$(echo "$base_payload" | jq '. + {
|
||||
format: "json",
|
||||
stream: false,
|
||||
options: {temperature: (.temperature | tonumber)}
|
||||
}')
|
||||
;;
|
||||
"LMSTUDIO")
|
||||
payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}')
|
||||
;;
|
||||
*)
|
||||
payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}')
|
||||
;;
|
||||
esac
|
||||
echo "$payload"
|
||||
}
|
||||
|
||||
log_request() {
|
||||
local user_input response_body user_input_hash log_file prompt_tokens completion_tokens created api_cost
|
||||
local prompt_tokens_int completion_tokens_int
|
||||
user_input="$1"
|
||||
response_body="$2"
|
||||
user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1)
|
||||
|
||||
prompt_tokens=$(echo "$response_body" | jq -r '.usage.prompt_tokens')
|
||||
prompt_tokens_int=$((prompt_tokens))
|
||||
completion_tokens=$(echo "$response_body" | jq -r '.usage.completion_tokens')
|
||||
completion_tokens_int=$((completion_tokens))
|
||||
|
||||
created=$(date +%s)
|
||||
created=$(echo "$response_body" | jq -r ".created // $created")
|
||||
api_cost=$(echo "$prompt_tokens_int * $ACSH_API_PROMPT_COST + $completion_tokens_int * $ACSH_API_COMPLETION_COST" | bc)
|
||||
log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.log"}
|
||||
echo "$created,$user_input_hash,$prompt_tokens_int,$completion_tokens_int,$api_cost" >> "$log_file"
|
||||
}
|
||||
|
||||
openai_completion() {
|
||||
local content status_code response_body default_user_input user_input api_key payload endpoint timeout attempt max_attempts
|
||||
endpoint=${ACSH_ENDPOINT:-"http://localhost:1234/v1/chat/completions"}
|
||||
timeout=${ACSH_TIMEOUT:-30}
|
||||
default_user_input="Write two to six most likely commands given the provided information"
|
||||
user_input=${*:-$default_user_input}
|
||||
|
||||
if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then
|
||||
echo_error "ACSH_ACTIVE_API_KEY not set. Please set it with: export ${ACSH_PROVIDER^^}_API_KEY=<your-api-key>"
|
||||
return
|
||||
fi
|
||||
api_key="${ACSH_ACTIVE_API_KEY}"
|
||||
payload=$(_build_payload "$user_input")
|
||||
|
||||
max_attempts=2
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then
|
||||
response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" --data "$payload")
|
||||
else
|
||||
response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload")
|
||||
fi
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
response_body=$(echo "$response" | sed '$d')
|
||||
if [[ $status_code -eq 200 ]]; then
|
||||
break
|
||||
else
|
||||
echo_error "API call failed with status $status_code. Retrying... (Attempt $attempt of $max_attempts)"
|
||||
sleep 1
|
||||
attempt=$((attempt+1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $status_code -ne 200 ]]; then
|
||||
case $status_code in
|
||||
400) echo_error "Bad Request: The API request was invalid or malformed." ;;
|
||||
401) echo_error "Unauthorized: The provided API key is invalid or missing." ;;
|
||||
429) echo_error "Too Many Requests: The API rate limit has been exceeded." ;;
|
||||
500) echo_error "Internal Server Error: An unexpected error occurred on the API server." ;;
|
||||
*) echo_error "Unknown Error: Unexpected status code $status_code received. Response: $response_body" ;;
|
||||
esac
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then
|
||||
content=$(echo "$response_body" | jq -r '.message.content')
|
||||
content=$(echo "$content" | jq -r '.completions')
|
||||
else
|
||||
content=$(echo "$response_body" | jq -r '.choices[0].message.content')
|
||||
content=$(echo "$content" | jq -r '.completions')
|
||||
fi
|
||||
|
||||
local completions
|
||||
completions=$(echo "$content" | jq -r '.[]' | grep -v '^$')
|
||||
echo -n "$completions"
|
||||
log_request "$user_input" "$response_body"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Completion Functions #
|
||||
###############################################################################
|
||||
|
||||
_get_default_completion_function() {
|
||||
local cmd="$1"
|
||||
complete -p "$cmd" 2>/dev/null | awk -F' ' '{ for(i=1;i<=NF;i++) { if ($i ~ /^-F$/) { print $(i+1); exit; } } }'
|
||||
}
|
||||
|
||||
_default_completion() {
|
||||
local current_word="" first_word="" default_func
|
||||
if [[ -n "${COMP_WORDS[*]}" ]]; then
|
||||
first_word="${COMP_WORDS[0]}"
|
||||
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
|
||||
current_word="${COMP_WORDS[COMP_CWORD]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
default_func=$(_get_default_completion_function "$first_word")
|
||||
if [[ -n "$default_func" ]]; then
|
||||
"$default_func"
|
||||
else
|
||||
local file_completions
|
||||
if [[ -z "$current_word" ]]; then
|
||||
file_completions=$(compgen -f -- || true)
|
||||
else
|
||||
file_completions=$(compgen -f -- "$current_word" || true)
|
||||
fi
|
||||
if [[ -n "$file_completions" ]]; then
|
||||
readarray -t COMPREPLY <<<"$file_completions"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
list_cache() {
|
||||
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"}
|
||||
find "$cache_dir" -maxdepth 1 -type f -name "acsh-*" -printf '%T+ %p\n' | sort
|
||||
}
|
||||
|
||||
_finishtesh() {
|
||||
_init_completion || return
|
||||
_default_completion
|
||||
if [[ ${#COMPREPLY[@]} -eq 0 && $COMP_TYPE -eq 63 ]]; then
|
||||
local completions user_input user_input_hash
|
||||
acsh_load_config
|
||||
if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then
|
||||
local provider_key="${ACSH_PROVIDER}_API_KEY"
|
||||
provider_key=$(echo "$provider_key" | tr '[:lower:]' '[:upper:]')
|
||||
echo_error "${provider_key} is not set. Please set it using: export ${provider_key}=<your-api-key> or disable finish via: finish disable"
|
||||
echo
|
||||
return
|
||||
fi
|
||||
if [[ -n "${COMP_WORDS[*]}" ]]; then
|
||||
command="${COMP_WORDS[0]}"
|
||||
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
|
||||
current="${COMP_WORDS[COMP_CWORD]}"
|
||||
fi
|
||||
fi
|
||||
user_input="${COMP_LINE:-"$command $current"}"
|
||||
user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1)
|
||||
export ACSH_INPUT="$user_input"
|
||||
export ACSH_PROMPT=
|
||||
export ACSH_RESPONSE=
|
||||
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"}
|
||||
local cache_size=${ACSH_CACHE_SIZE:-100}
|
||||
local cache_file="$cache_dir/acsh-$user_input_hash.txt"
|
||||
if [[ -d "$cache_dir" && "$cache_size" -gt 0 && -f "$cache_file" ]]; then
|
||||
completions=$(cat "$cache_file" || true)
|
||||
touch "$cache_file"
|
||||
else
|
||||
echo -en "\e]12;green\a"
|
||||
completions=$(openai_completion "$user_input" || true)
|
||||
if [[ -z "$completions" ]]; then
|
||||
echo -en "\e]12;red\a"
|
||||
sleep 1
|
||||
completions=$(openai_completion "$user_input" || true)
|
||||
fi
|
||||
echo -en "\e]12;white\a"
|
||||
if [[ -d "$cache_dir" && "$cache_size" -gt 0 ]]; then
|
||||
echo "$completions" > "$cache_file"
|
||||
while [[ $(list_cache | wc -l) -gt "$cache_size" ]]; do
|
||||
oldest=$(list_cache | head -n 1 | cut -d ' ' -f 2-)
|
||||
rm "$oldest" || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
export ACSH_RESPONSE=$completions
|
||||
if [[ -n "$completions" ]]; then
|
||||
local num_rows
|
||||
num_rows=$(echo "$completions" | wc -l)
|
||||
COMPREPLY=()
|
||||
if [[ $num_rows -eq 1 ]]; then
|
||||
readarray -t COMPREPLY <<<"$(echo -n "${completions}" | sed "s/${command}[[:space:]]*//" | sed 's/:/\\:/g')"
|
||||
else
|
||||
completions=$(echo "$completions" | awk '{print NR". "$0}')
|
||||
readarray -t COMPREPLY <<< "$completions"
|
||||
fi
|
||||
fi
|
||||
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
|
||||
COMPREPLY=("$current")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# CLI Commands & Configuration Management #
|
||||
###############################################################################
|
||||
|
||||
show_help() {
|
||||
echo_green "finish.sh - LLM Powered Bash Completion"
|
||||
echo "Usage: finish [options] command"
|
||||
echo " finish [options] install|remove|config|model|enable|disable|clear|usage|system|command|--help"
|
||||
echo
|
||||
echo "finish.sh enhances bash completion with LLM capabilities."
|
||||
echo "Press Tab twice for suggestions."
|
||||
echo "Commands:"
|
||||
echo " command Run finish (simulate double Tab)"
|
||||
echo " command --dry-run Show prompt without executing"
|
||||
echo " model Change language model"
|
||||
echo " usage Display usage stats"
|
||||
echo " system Display system information"
|
||||
echo " config Show or set configuration values"
|
||||
echo " config set <key> <value> Set a config value"
|
||||
echo " config reset Reset config to defaults"
|
||||
echo " install Install finish to .bashrc"
|
||||
echo " remove Remove installation from .bashrc"
|
||||
echo " enable Enable finish"
|
||||
echo " disable Disable finish"
|
||||
echo " clear Clear cache and log files"
|
||||
echo " --help Show this help message"
|
||||
echo
|
||||
echo "Submit issues at: https://git.appmodel.nl/Tour/finish/issues"
|
||||
}
|
||||
|
||||
is_subshell() {
|
||||
if [[ "$$" != "$BASHPID" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_config() {
|
||||
local config_file="$HOME/.finish/config" term_width small_table
|
||||
echo_green "finish.sh - Configuration and Settings - Version $ACSH_VERSION"
|
||||
if is_subshell; then
|
||||
echo " STATUS: Unknown. Run 'source finish config' to check status."
|
||||
return
|
||||
elif check_if_enabled; then
|
||||
echo -e " STATUS: \033[32;5mEnabled\033[0m"
|
||||
else
|
||||
echo -e " STATUS: \033[31;5mDisabled\033[0m - Run 'source finish config' to verify."
|
||||
fi
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo_error "Configuration file not found: $config_file. Run finish install."
|
||||
return
|
||||
fi
|
||||
acsh_load_config
|
||||
term_width=$(tput cols)
|
||||
if [[ $term_width -gt 70 ]]; then
|
||||
term_width=70; small_table=0
|
||||
fi
|
||||
if [[ $term_width -lt 40 ]]; then
|
||||
term_width=70; small_table=1
|
||||
fi
|
||||
for config_var in $(compgen -v | grep ACSH_); do
|
||||
if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then
|
||||
continue
|
||||
fi
|
||||
config_value="${!config_var}"
|
||||
if [[ ${config_var: -8} == "_API_KEY" ]]; then
|
||||
continue
|
||||
fi
|
||||
echo -en " $config_var:\e[90m"
|
||||
if [[ $small_table -eq 1 ]]; then
|
||||
echo -e "\n $config_value\e[0m"
|
||||
else
|
||||
printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) ''
|
||||
echo -e "$config_value\e[0m"
|
||||
fi
|
||||
done
|
||||
echo -e " ===================================================================="
|
||||
for config_var in $(compgen -v | grep ACSH_); do
|
||||
if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ ${config_var: -8} != "_API_KEY" ]]; then
|
||||
continue
|
||||
fi
|
||||
echo -en " $config_var:\e[90m"
|
||||
if [[ -z ${!config_var} ]]; then
|
||||
config_value="UNSET"
|
||||
echo -en "\e[31m"
|
||||
else
|
||||
rest=${!config_var:4}
|
||||
config_value="${!config_var:0:4}...${rest: -4}"
|
||||
echo -en "\e[32m"
|
||||
fi
|
||||
if [[ $small_table -eq 1 ]]; then
|
||||
echo -e "\n $config_value\e[0m"
|
||||
else
|
||||
printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) ''
|
||||
echo -e "$config_value\e[0m"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
set_config() {
|
||||
local key="$1" value="$2" config_file="$HOME/.finish/config"
|
||||
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')
|
||||
if [ -z "$key" ]; then
|
||||
echo_error "SyntaxError: expected 'finish config set <key> <value>'"
|
||||
return
|
||||
fi
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo_error "Configuration file not found: $config_file. Run finishte install."
|
||||
return
|
||||
fi
|
||||
sed -i "s|^\($key:\).*|\1 $value|" "$config_file"
|
||||
acsh_load_config
|
||||
}
|
||||
|
||||
config_command() {
|
||||
local command config_file="$HOME/.finishte/config"
|
||||
command="${*:2}"
|
||||
if [ -z "$command" ]; then
|
||||
show_config
|
||||
return
|
||||
fi
|
||||
if [ "$2" == "set" ]; then
|
||||
local key="$3" value="$4"
|
||||
echo "Setting configuration key '$key' to '$value'"
|
||||
set_config "$key" "$value"
|
||||
echo_green "Configuration updated. Run 'finishte config' to view changes."
|
||||
return
|
||||
fi
|
||||
if [[ "$command" == "reset" ]]; then
|
||||
echo "Resetting configuration to default values."
|
||||
rm "$config_file" || true
|
||||
build_config
|
||||
return
|
||||
fi
|
||||
echo_error "SyntaxError: expected 'finishte config set <key> <value>' or 'finishte config reset'"
|
||||
}
|
||||
|
||||
build_config() {
|
||||
local config_file="$HOME/.finishte/config" default_config
|
||||
if [ ! -f "$config_file" ]; then
|
||||
echo "Creating default configuration file at ~/.finishte/config"
|
||||
default_config="# ~/.finishte/config
|
||||
|
||||
# Model configuration
|
||||
provider: lmstudio
|
||||
model: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2
|
||||
temperature: 0.0
|
||||
endpoint: http://localhost:1234/v1/chat/completions
|
||||
api_prompt_cost: 0.000000
|
||||
api_completion_cost: 0.000000
|
||||
|
||||
# Max history and recent files
|
||||
max_history_commands: 20
|
||||
max_recent_files: 20
|
||||
|
||||
# Cache settings
|
||||
cache_dir: $HOME/.finishte/cache
|
||||
cache_size: 10
|
||||
|
||||
# Logging settings
|
||||
log_file: $HOME/.finishte/finishte.log"
|
||||
echo "$default_config" > "$config_file"
|
||||
fi
|
||||
}
|
||||
|
||||
acsh_load_config() {
|
||||
local config_file="$HOME/.finishte/config" key value
|
||||
if [ -f "$config_file" ]; then
|
||||
while IFS=':' read -r key value; do
|
||||
if [[ $key =~ ^# ]] || [[ -z $key ]]; then
|
||||
continue
|
||||
fi
|
||||
key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')
|
||||
if [[ -n $value ]]; then
|
||||
export "ACSH_$key"="$value"
|
||||
fi
|
||||
done < "$config_file"
|
||||
if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$LLM_API_KEY" ]]; then
|
||||
export ACSH_OLLAMA_API_KEY="$LLM_API_KEY"
|
||||
fi
|
||||
# If the custom API key was set, map it to OLLAMA if needed.
|
||||
if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$ACSH_CUSTOM_API_KEY" ]]; then
|
||||
export ACSH_OLLAMA_API_KEY="$ACSH_CUSTOM_API_KEY"
|
||||
fi
|
||||
case "${ACSH_PROVIDER:-lmstudio}" in
|
||||
"ollama") export ACSH_ACTIVE_API_KEY="$ACSH_OLLAMA_API_KEY" ;;
|
||||
"lmstudio") export ACSH_ACTIVE_API_KEY="" ;;
|
||||
*) echo_error "Unknown provider: $ACSH_PROVIDER" ;;
|
||||
esac
|
||||
else
|
||||
echo "Configuration file not found: $config_file"
|
||||
fi
|
||||
}
|
||||
|
||||
install_command() {
|
||||
local bashrc_file="$HOME/.bashrc" finishte_setup="source finishte enable" finishte_cli_setup="complete -F _finishtesh_cli finishte"
|
||||
if ! command -v finishte &>/dev/null; then
|
||||
echo_error "finishte.sh not in PATH. Follow install instructions at https://git.appmodel.nl/Tour/finish"
|
||||
return
|
||||
fi
|
||||
if [[ ! -d "$HOME/.finishte" ]]; then
|
||||
echo "Creating ~/.finishte directory"
|
||||
mkdir -p "$HOME/.finishte"
|
||||
fi
|
||||
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"}
|
||||
if [[ ! -d "$cache_dir" ]]; then
|
||||
mkdir -p "$cache_dir"
|
||||
fi
|
||||
build_config
|
||||
acsh_load_config
|
||||
if ! grep -qF "$finishte_setup" "$bashrc_file"; then
|
||||
echo -e "# finishte.sh" >> "$bashrc_file"
|
||||
echo -e "$finishte_setup\n" >> "$bashrc_file"
|
||||
echo "Added finishte.sh setup to $bashrc_file"
|
||||
else
|
||||
echo "finishte.sh setup already exists in $bashrc_file"
|
||||
fi
|
||||
if ! grep -qF "$finishte_cli_setup" "$bashrc_file"; then
|
||||
echo -e "# finishte.sh CLI" >> "$bashrc_file"
|
||||
echo -e "$finishte_cli_setup\n" >> "$bashrc_file"
|
||||
echo "Added finishte CLI completion to $bashrc_file"
|
||||
fi
|
||||
echo
|
||||
echo_green "finishte.sh - Version $ACSH_VERSION installation complete."
|
||||
echo -e "Run: source $bashrc_file to enable finishte."
|
||||
echo -e "Then run: finishte model to select a language model."
|
||||
}
|
||||
|
||||
remove_command() {
|
||||
local config_file="$HOME/.finishte/config" cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} bashrc_file="$HOME/.bashrc"
|
||||
echo_green "Removing finishte.sh installation..."
|
||||
[ -f "$config_file" ] && { rm "$config_file"; echo "Removed: $config_file"; }
|
||||
[ -d "$cache_dir" ] && { rm -rf "$cache_dir"; echo "Removed: $cache_dir"; }
|
||||
[ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; }
|
||||
if [ -d "$HOME/.finishte" ]; then
|
||||
if [ -z "$(ls -A "$HOME/.finishte")" ]; then
|
||||
rmdir "$HOME/.finishte"
|
||||
echo "Removed: $HOME/.finishte"
|
||||
else
|
||||
echo "Skipped removing $HOME/.finishte (not empty)"
|
||||
fi
|
||||
fi
|
||||
if [ -f "$bashrc_file" ]; then
|
||||
if grep -qF "source finishte enable" "$bashrc_file"; then
|
||||
sed -i '/# finishte.sh/d' "$bashrc_file"
|
||||
sed -i '/finishte/d' "$bashrc_file"
|
||||
echo "Removed finishte.sh setup from $bashrc_file"
|
||||
fi
|
||||
fi
|
||||
local finishte_script
|
||||
finishte_script=$(command -v finishte)
|
||||
if [ -n "$finishte_script" ]; then
|
||||
echo "finishte script is at: $finishte_script"
|
||||
if [ "$1" == "-y" ]; then
|
||||
rm "$finishte_script"
|
||||
echo "Removed: $finishte_script"
|
||||
else
|
||||
read -r -p "Remove the finishte script? (y/n): " confirm
|
||||
if [[ $confirm == "y" ]]; then
|
||||
rm "$finishte_script"
|
||||
echo "Removed: $finishte_script"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "Uninstallation complete."
|
||||
}
|
||||
|
||||
check_if_enabled() {
|
||||
local is_enabled
|
||||
is_enabled=$(complete -p | grep _finishtesh | grep -cv _finishtesh_cli)
|
||||
(( is_enabled > 0 )) && return 0 || return 1
|
||||
}
|
||||
|
||||
_finishtesh_cli() {
|
||||
if [[ -n "${COMP_WORDS[*]}" ]]; then
|
||||
command="${COMP_WORDS[0]}"
|
||||
if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then
|
||||
current="${COMP_WORDS[COMP_CWORD]}"
|
||||
fi
|
||||
fi
|
||||
if [[ $current == "config" ]]; then
|
||||
readarray -t COMPREPLY <<< "set
|
||||
reset"
|
||||
return
|
||||
elif [[ $current == "command" ]]; then
|
||||
readarray -t COMPREPLY <<< "command --dry-run"
|
||||
return
|
||||
fi
|
||||
if [[ -z "$current" ]]; then
|
||||
readarray -t COMPREPLY <<< "install
|
||||
remove
|
||||
config
|
||||
enable
|
||||
disable
|
||||
clear
|
||||
usage
|
||||
system
|
||||
command
|
||||
model
|
||||
--help"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_command() {
|
||||
if check_if_enabled; then
|
||||
echo_green "Reloading finishte.sh..."
|
||||
disable_command
|
||||
fi
|
||||
acsh_load_config
|
||||
complete -D -E -F _finishtesh -o nospace
|
||||
}
|
||||
|
||||
disable_command() {
|
||||
if check_if_enabled; then
|
||||
complete -F _completion_loader -D
|
||||
fi
|
||||
}
|
||||
|
||||
command_command() {
|
||||
local args=("$@")
|
||||
for ((i = 0; i < ${#args[@]}; i++)); do
|
||||
if [ "${args[i]}" == "--dry-run" ]; then
|
||||
args[i]=""
|
||||
_build_prompt "${args[@]}"
|
||||
return
|
||||
fi
|
||||
done
|
||||
openai_completion "$@" || true
|
||||
echo
|
||||
}
|
||||
|
||||
clear_command() {
|
||||
local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"}
|
||||
echo "This will clear the cache and log file."
|
||||
echo -e "Cache directory: \e[31m$cache_dir\e[0m"
|
||||
echo -e "Log file: \e[31m$log_file\e[0m"
|
||||
read -r -p "Are you sure? (y/n): " confirm
|
||||
if [[ $confirm != "y" ]]; then
|
||||
echo "Aborted."
|
||||
return
|
||||
fi
|
||||
if [ -d "$cache_dir" ]; then
|
||||
local cache_files
|
||||
cache_files=$(list_cache)
|
||||
if [ -n "$cache_files" ]; then
|
||||
while read -r line; do
|
||||
file=$(echo "$line" | cut -d ' ' -f 2-)
|
||||
rm "$file"
|
||||
echo "Removed: $file"
|
||||
done <<< "$cache_files"
|
||||
echo "Cleared cache in: $cache_dir"
|
||||
else
|
||||
echo "Cache is empty."
|
||||
fi
|
||||
fi
|
||||
[ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; }
|
||||
}
|
||||
|
||||
usage_command() {
|
||||
local log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"}
|
||||
local cache_size number_of_lines api_cost avg_api_cost
|
||||
cache_size=$(list_cache | wc -l)
|
||||
echo_green "finishte.sh - Usage Information"
|
||||
echo
|
||||
echo -n "Log file: "; echo -e "\e[90m$log_file\e[0m"
|
||||
if [ ! -f "$log_file" ]; then
|
||||
number_of_lines=0
|
||||
api_cost=0
|
||||
avg_api_cost=0
|
||||
else
|
||||
number_of_lines=$(wc -l < "$log_file")
|
||||
api_cost=$(awk -F, '{sum += $5} END {print sum}' "$log_file")
|
||||
avg_api_cost=$(echo "$api_cost / $number_of_lines" | bc -l)
|
||||
fi
|
||||
echo
|
||||
echo -e "\tUsage count:\t\e[32m$number_of_lines\e[0m"
|
||||
echo -e "\tAvg Cost:\t\$$(printf "%.4f" "$avg_api_cost")"
|
||||
echo -e "\tTotal Cost:\t\e[31m\$$(printf "%.4f" "$api_cost")\e[0m"
|
||||
echo
|
||||
echo -n "Cache Size: $cache_size of ${ACSH_CACHE_SIZE:-10} in "; echo -e "\e[90m$cache_dir\e[0m"
|
||||
echo "To clear log and cache, run: finishte clear"
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Enhanced Interactive Menu UX #
|
||||
###############################################################################
|
||||
|
||||
get_key() {
|
||||
IFS= read -rsn1 key 2>/dev/null >&2
|
||||
if [[ $key == $'\x1b' ]]; then
|
||||
read -rsn2 key
|
||||
if [[ $key == [A ]]; then echo up; fi
|
||||
if [[ $key == [B ]]; then echo down; fi
|
||||
if [[ $key == q ]]; then echo q; fi
|
||||
elif [[ $key == "q" ]]; then
|
||||
echo q
|
||||
else
|
||||
echo "$key"
|
||||
fi
|
||||
}
|
||||
|
||||
menu_selector() {
|
||||
options=("$@")
|
||||
selected=0
|
||||
show_menu() {
|
||||
echo
|
||||
echo "Select a Language Model (Up/Down arrows, Enter to select, 'q' to quit):"
|
||||
for i in "${!options[@]}"; do
|
||||
if [[ $i -eq $selected ]]; then
|
||||
echo -e "\e[1;32m> ${options[i]}\e[0m"
|
||||
else
|
||||
echo " ${options[i]}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
tput sc
|
||||
while true; do
|
||||
tput rc; tput ed
|
||||
show_menu
|
||||
key=$(get_key)
|
||||
case $key in
|
||||
up)
|
||||
((selected--))
|
||||
if ((selected < 0)); then
|
||||
selected=$((${#options[@]} - 1))
|
||||
fi
|
||||
;;
|
||||
down)
|
||||
((selected++))
|
||||
if ((selected >= ${#options[@]})); then
|
||||
selected=0
|
||||
fi
|
||||
;;
|
||||
q)
|
||||
echo "Selection canceled."
|
||||
return 1
|
||||
;;
|
||||
"")
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
clear
|
||||
return $selected
|
||||
}
|
||||
|
||||
model_command() {
|
||||
clear
|
||||
local selected_model options=()
|
||||
if [[ $# -ne 3 ]]; then
|
||||
mapfile -t sorted_keys < <(for key in "${!_finishte_modellist[@]}"; do echo "$key"; done | sort)
|
||||
for key in "${sorted_keys[@]}"; do
|
||||
options+=("$key")
|
||||
done
|
||||
echo -e "\e[1;32mfinishte.sh - Model Configuration\e[0m"
|
||||
menu_selector "${options[@]}"
|
||||
selected_option=$?
|
||||
if [[ $selected_option -eq 1 ]]; then
|
||||
return
|
||||
fi
|
||||
selected_model="${options[selected_option]}"
|
||||
selected_value="${_finishte_modellist[$selected_model]}"
|
||||
else
|
||||
provider="$2"
|
||||
model_name="$3"
|
||||
selected_value="${_finishte_modellist["$provider: $model_name"]}"
|
||||
if [[ -z "$selected_value" ]]; then
|
||||
echo "ERROR: Invalid provider or model name."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
set_config "model" "$(echo "$selected_value" | jq -r '.model')"
|
||||
set_config "endpoint" "$(echo "$selected_value" | jq -r '.endpoint')"
|
||||
set_config "provider" "$(echo "$selected_value" | jq -r '.provider')"
|
||||
prompt_cost=$(echo "$selected_value" | jq -r '.prompt_cost' | awk '{printf "%.8f", $1}')
|
||||
completion_cost=$(echo "$selected_value" | jq -r '.completion_cost' | awk '{printf "%.8f", $1}')
|
||||
set_config "api_prompt_cost" "$prompt_cost"
|
||||
set_config "api_completion_cost" "$completion_cost"
|
||||
model="${ACSH_MODEL:-ERROR}"
|
||||
temperature=$(echo "${ACSH_TEMPERATURE:-0.0}" | awk '{printf "%.3f", $1}')
|
||||
echo -e "Provider:\t\e[90m$ACSH_PROVIDER\e[0m"
|
||||
echo -e "Model:\t\t\e[90m$model\e[0m"
|
||||
echo -e "Temperature:\t\e[90m$temperature\e[0m"
|
||||
echo
|
||||
echo -e "Cost/token:\t\e[90mprompt: \$$ACSH_API_PROMPT_COST, completion: \$$ACSH_API_COMPLETION_COST\e[0m"
|
||||
echo -e "Endpoint:\t\e[90m$ACSH_ENDPOINT\e[0m"
|
||||
if [[ ${ACSH_PROVIDER^^} == "OLLAMA" || ${ACSH_PROVIDER^^} == "LMSTUDIO" ]]; then
|
||||
echo "To set a custom endpoint:"
|
||||
echo -e "\t\e[34mfinishte config set endpoint <your-url>\e[0m"
|
||||
echo "Other models can be set with:"
|
||||
echo -e "\t\e[34mfinishte config set model <model-name>\e[0m"
|
||||
fi
|
||||
echo "To change temperature:"
|
||||
echo -e "\t\e[90mfinishte config set temperature <temperature>\e[0m"
|
||||
echo
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# CLI ENTRY POINT #
|
||||
###############################################################################
|
||||
|
||||
case "$1" in
|
||||
"--help")
|
||||
show_help
|
||||
;;
|
||||
system)
|
||||
_system_info
|
||||
;;
|
||||
install)
|
||||
install_command
|
||||
;;
|
||||
remove)
|
||||
remove_command "$@"
|
||||
;;
|
||||
clear)
|
||||
clear_command
|
||||
;;
|
||||
usage)
|
||||
usage_command
|
||||
;;
|
||||
model)
|
||||
model_command "$@"
|
||||
;;
|
||||
config)
|
||||
config_command "$@"
|
||||
;;
|
||||
enable)
|
||||
enable_command
|
||||
;;
|
||||
disable)
|
||||
disable_command
|
||||
;;
|
||||
command)
|
||||
command_command "$@"
|
||||
;;
|
||||
*)
|
||||
if [[ -n "$1" ]]; then
|
||||
echo_error "Unknown command $1 - run 'finishte --help' for usage or visit https://finishte.sh"
|
||||
else
|
||||
echo_green "finishte.sh - LLM Powered Bash Completion - Version $ACSH_VERSION - https://finishte.sh"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user