From 6ea1e6c00c7f533c90218b5b55bbb86bd50731f8 Mon Sep 17 00:00:00 2001 From: mike Date: Mon, 29 Dec 2025 22:17:35 +0100 Subject: [PATCH] chore: update 9 file(s) --- .gitignore | 2 + LICENSE | 24 - README.md | 4 +- debian/copyright | 30 -- docker-compose.yml | 2 +- finish.iml | 12 - fish.txt | 1 - src/finish.py | 2 +- src/finish.sh | 1072 -------------------------------------------- 9 files changed, 6 insertions(+), 1143 deletions(-) delete mode 100644 LICENSE delete mode 100644 debian/copyright delete mode 100644 finish.iml delete mode 100644 fish.txt delete mode 100755 src/finish.sh diff --git a/.gitignore b/.gitignore index c70d8c5..17d7dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ __pycache__/ *.tmp temp/ dist/ +.aider* +*.iml diff --git a/LICENSE b/LICENSE deleted file mode 100644 index cd88cb4..0000000 --- a/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -BSD 2-Clause License - -Copyright (c) 2025, finish contributors - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index fbb12f6..c29a2a8 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ sudo apt-get install python3-venv # Then run the installer curl -sSL https://git.appmodel.nl/tour/finish/raw/branch/main/docs/install.sh | bash -source ~/.bashrc # or ~/.zshrc +source ~/.bashrc # or ~/.zshrc # for plato special, add plato.lan to /etc/hosts: -sudo nano /etc/hosts - 192.168.1.74 plato.lan +sudo nano /etc/hosts - 192.168.1.74 plato.lan ``` Press **Alt+\\** after typing any command to get intelligent completions—no cloud, no data leak, instant results. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 53539c6..0000000 --- a/debian/copyright +++ /dev/null @@ -1,30 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: finish -Upstream-Contact: https://git.appmodel.nl/tour/finish/finish -Source: https://git.appmodel.nl/tour/finish - -Files: * -Copyright: 2024-2025 finish.sh Contributors -License: BSD-2-Clause - -License: BSD-2-Clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - . - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - . - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docker-compose.yml b/docker-compose.yml index e7ebbcd..e11f507 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: volumes: - finish-data:/root/.finish environment: - - ACSH_ENDPOINT=http://plato.lan:1234/v1/chat/completions + - ACSH_ENDPOINT=http://llm.plato/v1/chat/completions extra_hosts: - "host.docker.internal:host-gateway" command: /bin/bash diff --git a/finish.iml b/finish.iml deleted file mode 100644 index 4560575..0000000 --- a/finish.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/fish.txt b/fish.txt deleted file mode 100644 index 2425b32..0000000 --- a/fish.txt +++ /dev/null @@ -1 +0,0 @@ - ><(((('> diff --git a/src/finish.py b/src/finish.py index f6bbe9d..951487c 100755 --- a/src/finish.py +++ b/src/finish.py @@ -34,7 +34,7 @@ console = Console() DEFAULT_CFG = { "provider": "ollama", "model": "llama3:latest", - "endpoint": "http://localhost:11434/api/chat", + "endpoint": os.getenv("LLM_FINISH_URL", "http://localhost:11434/api/chat"), "temperature": 0.0, "api_prompt_cost": 0.0, "api_completion_cost": 0.0, diff --git a/src/finish.sh b/src/finish.sh deleted file mode 100755 index 3b83d27..0000000 --- a/src/finish.sh +++ /dev/null @@ -1,1072 +0,0 @@ -#!/bin/bash -############################################################################### -# -ver 1- 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[31mfinish.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 _finish_modellist -declare -A _finish_modellist -# LM-Studio models -_finish_modellist['lmstudio: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://plato.lan:1234/v1/chat/completions", "model": "darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2", "provider": "lmstudio" }' -# Ollama model -_finish_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. - -CRITICAL: You MUST respond with ONLY a valid JSON object in this EXACT format with no additional text before or after: -{\"completions\": [\"command1\", \"command2\", \"command3\"]} - -Example response: -{\"completions\": [\"ls -la\", \"ls -lh\", \"find . -type f\"]}" -} - -_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 <&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 '. + { - max_tokens: -1, - stream: true - }') - ;; - *) - # Default OpenAI-compatible providers increasingly expect - # response_format.type to be either "text" or a json_schema. - # Use "text" for maximum compatibility to avoid 400 errors. - payload=$(echo "$base_payload" | jq '. + {response_format: {type: "text"}}') - ;; - 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"} - mkdir -p "$(dirname "$log_file")" 2>/dev/null - 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 - local log_file debug_log - - # Ensure configuration (provider, endpoint, etc.) is loaded before checks - acsh_load_config - - log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.log"} - debug_log="$HOME/.finish/debug.log" - endpoint=${ACSH_ENDPOINT:-"http://plato.lan: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} - - # Only check for API key if not using local providers that don't require it - 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=" - return 1 - fi - api_key="${ACSH_ACTIVE_API_KEY}" - payload=$(_build_payload "$user_input") - - # Debug logging - echo "=== DEBUG LOG $(date) ===" >> "$debug_log" - echo "Provider: ${ACSH_PROVIDER}" >> "$debug_log" - echo "Endpoint: $endpoint" >> "$debug_log" - echo "User input: $user_input" >> "$debug_log" - echo "Payload:" >> "$debug_log" - echo "$payload" >> "$debug_log" - echo "" >> "$debug_log" - - max_attempts=2 - attempt=1 - local stream_enabled=false - - # Check if streaming is enabled in payload - if echo "$payload" | jq -e '.stream == true' > /dev/null 2>&1; then - stream_enabled=true - fi - - while [ $attempt -le $max_attempts ]; do - if [[ "$stream_enabled" == true ]]; then - # Streaming mode - collect chunks and display in real-time - local temp_file=$(mktemp) - local stream_content="" - - if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then - \curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" --data "$payload" -N > "$temp_file" - else - if [[ -n "$api_key" ]]; then - \curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $api_key" \ - -d "$payload" -N > "$temp_file" - else - \curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \ - -H "Content-Type: application/json" \ - -d "$payload" -N > "$temp_file" - fi - fi - - status_code=$(tail -n1 "$temp_file") - response_body=$(sed '$d' "$temp_file") - - # Parse SSE stream and extract content - while IFS= read -r line; do - if [[ "$line" =~ ^data:\ (.+)$ ]]; then - chunk_data="${BASH_REMATCH[1]}" - if [[ "$chunk_data" != "[DONE]" ]]; then - chunk_content=$(echo "$chunk_data" | jq -r '.choices[0].delta.content // empty' 2>/dev/null) - if [[ -n "$chunk_content" && "$chunk_content" != "null" ]]; then - stream_content+="$chunk_content" - fi - fi - fi - done < <(echo "$response_body") - - # Store accumulated content as response_body for later processing - # Use jq to properly escape the content string - response_body=$(jq -n --arg content "$stream_content" '{choices: [{message: {content: $content}}]}') - rm -f "$temp_file" - else - # Non-streaming mode (original behavior) - if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then - response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" --data "$payload") - else - if [[ -n "$api_key" ]]; then - response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $api_key" \ - -d "$payload") - else - response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \ - -H "Content-Type: application/json" \ - -d "$payload") - fi - fi - status_code=$(echo "$response" | tail -n1) - response_body=$(echo "$response" | sed '$d') - fi - - # Debug logging - echo "Response status: $status_code" >> "$debug_log" - echo "Response body:" >> "$debug_log" - echo "$response_body" >> "$debug_log" - echo "" >> "$debug_log" - - if [[ $status_code -eq 200 ]]; then - break - else - echo_error "API call failed with status $status_code. Retrying... (Attempt $attempt of $max_attempts)" - echo "Error response: $response_body" >> "$debug_log" - 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. Check $debug_log for details." ;; - 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 1 -fi - - if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then - content=$(echo "$response_body" | jq -r '.message.content') - else - content=$(echo "$response_body" | jq -r '.choices[0].message.content') - fi - - # Log the raw content for debugging - echo "Raw content from API:" >> "$debug_log" - echo "$content" >> "$debug_log" - echo "" >> "$debug_log" - - # Try to parse as JSON first - local completions - if echo "$content" | jq -e '.completions' > /dev/null 2>&1; then - completions=$(echo "$content" | jq -r '.completions[]' | grep -v '^$') - else - # Fallback: try to extract JSON object from text - json_match=$(echo "$content" | grep -oP '\{[^}]*"completions"[^}]*\}' | head -1) - if [[ -n "$json_match" ]]; then - completions=$(echo "$json_match" | jq -r '.completions[]' 2>/dev/null | grep -v '^$') - else - # Last resort: parse line by line (skip explanatory text) - completions=$(echo "$content" | grep -v "^Here\|^These\|^The\|^Based" | grep -E "^(ls|cd|find|cat|grep|echo|mkdir|rm|cp|mv|pwd|chmod|chown)" | head -5) - fi - fi - - if [[ -z "$completions" ]]; then - echo_error "Failed to parse completions from API response. Check $debug_log for details." - return 1 - fi - - 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 - endpoint=${ACSH_ENDPOINT:-"http://plato.lan:1234/v1/chat/completions"} - 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 -} - -_finishsh() { - _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 [[ -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 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" lowercase_key - key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - lowercase_key=$(echo "$key" | tr '[:upper:]' '[:lower:]') - key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g') - if [ -z "$key" ]; then - echo_error "SyntaxError: expected 'finish config set '" - return - fi - if [ ! -f "$config_file" ]; then - echo_error "Configuration file not found: $config_file. Run finish install." - return - fi - sed -i "s|^\($lowercase_key:\).*|\1 $value|" "$config_file" - acsh_load_config -} - -config_command() { - local command config_file="$HOME/.finish/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 'finish 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 'finish config set ' or 'finish config reset'" -} - -build_config() { - local config_file="$HOME/.finish/config" default_config - if [ ! -f "$config_file" ]; then - echo "Creating default configuration file at ~/.finish/config" - default_config="# ~/.finish/config - -# Model configuration -provider: lmstudio -model: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2 -temperature: 0.0 -endpoint: http://plato.lan: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/.finish/cache -cache_size: 10 - -# Logging settings -log_file: $HOME/.finish/finish.log" - echo "$default_config" > "$config_file" - fi -} - -acsh_load_config() { - local config_file="$HOME/.finish/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" finish_setup="source finish enable" finish_cli_setup="complete -F _finishsh_cli finish" - if ! command -v finish &>/dev/null; then - echo_error "finish.sh not in PATH. Follow install instructions at https://git.appmodel.nl/tour/finish" - return - fi - if [[ ! -d "$HOME/.finish" ]]; then - echo "Creating ~/.finish directory" - mkdir -p "$HOME/.finish" - fi - local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"} - if [[ ! -d "$cache_dir" ]]; then - mkdir -p "$cache_dir" - fi - build_config - acsh_load_config - if ! grep -qF "$finish_setup" "$bashrc_file"; then - echo -e "# finish.sh" >> "$bashrc_file" - echo -e "$finish_setup\n" >> "$bashrc_file" - echo "Added finish.sh setup to $bashrc_file" - else - echo "finish.sh setup already exists in $bashrc_file" - fi - if ! grep -qF "$finish_cli_setup" "$bashrc_file"; then - echo -e "# finish.sh CLI" >> "$bashrc_file" - echo -e "$finish_cli_setup\n" >> "$bashrc_file" - echo "Added finish CLI completion to $bashrc_file" - fi - echo - echo_green "finish.sh - Version $ACSH_VERSION installation complete." - echo -e "Run: source $bashrc_file to enable finish." - echo -e "Then run: finish model to select a language model." -} - -remove_command() { - local config_file="$HOME/.finish/config" cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.log"} bashrc_file="$HOME/.bashrc" - echo_green "Removing finish.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/.finish" ]; then - if [ -z "$(ls -A "$HOME/.finish")" ]; then - rmdir "$HOME/.finish" - echo "Removed: $HOME/.finish" - else - echo "Skipped removing $HOME/.finish (not empty)" - fi - fi - if [ -f "$bashrc_file" ]; then - if grep -qF "source finish enable" "$bashrc_file"; then - sed -i '/# finish.sh/d' "$bashrc_file" - sed -i '/finish/d' "$bashrc_file" - echo "Removed finish.sh setup from $bashrc_file" - fi - fi - local finish_script - finish_script=$(command -v finish) - if [ -n "$finish_script" ]; then - echo "finish script is at: $finish_script" - if [ "$1" == "-y" ]; then - rm "$finish_script" - echo "Removed: $finish_script" - else - read -r -p "Remove the finish script? (y/n): " confirm - if [[ $confirm == "y" ]]; then - rm "$finish_script" - echo "Removed: $finish_script" - fi - fi - fi - echo "Uninstallation complete." -} - -check_if_enabled() { - local is_enabled - is_enabled=$(complete -p | grep _finishsh | grep -cv _finishsh_cli) - (( is_enabled > 0 )) && return 0 || return 1 -} - -_finishsh_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 finish.sh..." - disable_command - fi - acsh_load_config - complete -D -E -F _finishsh -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/.finish/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.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/.finish/finish.log"} cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"} - local cache_size number_of_lines api_cost avg_api_cost - cache_size=$(list_cache | wc -l) - echo_green "finish.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: finish 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 "${!_finish_modellist[@]}"; do echo "$key"; done | sort) - for key in "${sorted_keys[@]}"; do - options+=("$key") - done - echo -e "\e[1;32mfinish.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="${_finish_modellist[$selected_model]}" - else - provider="$2" - model_name="$3" - selected_value="${_finish_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" - endpoint=${ACSH_ENDPOINT:-"http://plato.lan:1234/v1/chat/completions"} - echo -e "Endpoint:\t\e[90m$endpoint\e[0m" - if [[ ${ACSH_PROVIDER^^} == "OLLAMA" || ${ACSH_PROVIDER^^} == "LMSTUDIO" ]]; then - echo "To set a custom endpoint:" - echo -e "\t\e[34mfinish config set endpoint \e[0m" - echo "Other models can be set with:" - echo -e "\t\e[34mfinish config set model \e[0m" - fi - echo "To change temperature:" - echo -e "\t\e[90mfinish config set 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 'finish --help' for usage or visit https://finish.sh" - else - echo_green "finish.sh - LLM Powered Bash Completion - Version $ACSH_VERSION - https://finish.sh" - fi - ;; -esac