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