first commit
Some checks failed
Docker Build and Push / build-and-push (push) Has been cancelled
Tests / test (bash) (push) Has been cancelled
Tests / test (zsh) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled

This commit is contained in:
2025-12-02 09:02:03 +01:00
commit 24d36cbad4
27 changed files with 2781 additions and 0 deletions

960
finish.sh Normal file
View 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-keylike 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