closedloop-technologies / autocomplete-sh

Large language model in the terminal! Less `--help` and `man` and more getting stuff done
MIT License
43 stars 2 forks source link

Support for ZSH #9

Open closedLoop opened 1 month ago

closedLoop commented 1 month ago

So I'd love to support zsh as well as bash

From Claude: Yes, there are several key differences between how Bash and Zsh implement command-line completions. Here are some of the main points to consider when migrating a bash_completion script to Zsh:

  1. Syntax:

    • Bash uses the complete command for defining completions.
    • Zsh uses the compdef function and the #compdef special comment.
  2. Completion functions:

    • In Bash, you typically define a function and then use complete -F to associate it with a command.
    • In Zsh, you define a function with a specific naming convention (usually _commandname) and use compdef to associate it.
  3. Context awareness:

    • Zsh's completion system is more context-aware and offers finer control over completions based on command position and argument type.
  4. Completion specification:

    • Zsh uses a more sophisticated completion specification system with tags and labels.
  5. Built-in completion functions:

    • Zsh provides many more built-in completion functions and utilities compared to Bash.
  6. Performance:

    • Zsh's completion system is generally faster and more efficient, especially for complex completions.
  7. Customization:

    • Zsh offers more extensive customization options for completion styles and behavior.
  8. Caching:

    • Zsh has built-in caching mechanisms for completions, which can improve performance for expensive operations.

When migrating your script, you'll need to rewrite your completion logic using Zsh's completion system. This often involves using Zsh-specific functions like _arguments, _describe, _values, and others.

Would you like me to provide an example of how to structure a basic Zsh completion function? Or do you have any specific aspects of your Bash completion script that you'd like guidance on converting?

closedLoop commented 1 month ago

Here's a claude rewrite of it. Lots of shellcheck bugs so I'm waiting to take a look at this

#!/bin/sh

# Autocomplete.sh - LLM Powered Zsh Completion
# Migrated from Bash version
# MIT License - ClosedLoop Technologies, Inc.
# Sean Kruzel 2024

export ACSH_VERSION=0.4.1

###############################################################################
#
# SUPPORTED MODELS AND MODEL PRICING
#
###############################################################################
typeset -A _autocomplete_modellist

# OpenAI models
_autocomplete_modellist['openai:    gpt-4o']='{"completion_cost":0.0000150,"prompt_cost":0.00000500,"endpoint":"https://api.openai.com/v1/chat/completions","model":"gpt-4o","provider":"openai"}'
_autocomplete_modellist['openai:    gpt-4o-mini']='{"completion_cost":0.0000006,"prompt_cost":0.00000015,"endpoint":"https://api.openai.com/v1/chat/completions","model":"gpt-4o-mini","provider":"openai"}'
_autocomplete_modellist['openai:    gpt-3.5-turbo-0125']='{"completion_cost":0.0000015,"prompt_cost":0.00000050,"endpoint":"https://api.openai.com/v1/chat/completions","model":"gpt-3.5-turbo-0125","provider":"openai"}'

# Anthropic models
_autocomplete_modellist['anthropic: claude-3-5-sonnet-20240620']='{"completion_cost":0.0000150,"prompt_cost":0.0000030,"endpoint":"https://api.anthropic.com/v1/messages","model":"claude-3-5-sonnet-20240620","provider":"anthropic"}'
_autocomplete_modellist['anthropic: claude-3-opus-20240229']='{"completion_cost":0.0000750,"prompt_cost":0.0000150,"endpoint":"https://api.anthropic.com/v1/messages","model":"claude-3-opus-20240229","provider":"anthropic"}'
_autocomplete_modellist['anthropic: claude-3-haiku-20240307']='{"completion_cost":0.00000125,"prompt_cost":0.00000025,"endpoint":"https://api.anthropic.com/v1/messages","model":"claude-3-haiku-20240307","provider":"anthropic"}'

# Groq models
_autocomplete_modellist['groq:      llama3-8b-8192']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"https://api.groq.com/openai/v1/chat/completions","model":"llama3-8b-8192","provider":"groq"}'
_autocomplete_modellist['groq:      llama3-70b-8192']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"https://api.groq.com/openai/v1/chat/completions","model":"llama3-70b-8192","provider":"groq"}'
_autocomplete_modellist['groq:      mixtral-8x7b-32768']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"https://api.groq.com/openai/v1/chat/completions","model":"mixtral-8x7b-32768","provider":"groq"}'
_autocomplete_modellist['groq:      gemma-7b-it']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"https://api.groq.com/openai/v1/chat/completions","model":"gemma-7b-it","provider":"groq"}'
_autocomplete_modellist['groq:      gemma2-9b-it']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"https://api.groq.com/openai/v1/chat/completions","model":"gemma2-9b-it","provider":"groq"}'

# Ollama models
_autocomplete_modellist['ollama:    codellama']='{"completion_cost":0.0000000,"prompt_cost":0.0000000,"endpoint":"http://localhost:11434/api/chat","model":"codellama","provider":"ollama"}'

###############################################################################
#
# FORMATTING FUNCTIONS
#
###############################################################################

function echo_error() {
    echo -e "\e[31mAutocomplete.sh - $1\e[0m" >&2
}

function echo_green() {
    echo -e "\e[32m$1\e[0m"
}

###############################################################################
#
# SYSTEM INFORMATION FUNCTIONS
#
###############################################################################

# Check if jq is installed
if ! command -v jq &>/dev/null; then
    echo_error "jq is not installed. Please install it using your package manager."
fi

function _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: \$ZSH_VERSION=$ZSH_VERSION
 * Terminal type: \$TERM=$TERM
 * Hostname: \$HOST=$HOST
"
    echo "$terminal_info"
}

# Generate a unique machine signature based on the hash of the uname and user
function machine_signature() {
    local signature
    signature=$(echo "$(uname -a)|$USER" | md5sum | cut -d ' ' -f 1)
    echo "$signature"
}

function _system_info() {
    echo "# System Information"
    echo
    uname -a
    echo "SIGNATURE: $(machine_signature)"
    echo
    echo "ZSH_VERSION: $ZSH_VERSION"
    echo
    echo "## Terminal Information"
    _get_terminal_info
}

function _completion_vars() {
    echo "CURRENT: $CURRENT"
    echo "words: ${words[*]}"
    echo "BUFFER: $BUFFER"
    echo "CURSOR: $CURSOR"
    echo "LBUFFER: $LBUFFER"
    echo "RBUFFER: $RBUFFER"
}

###############################################################################
#
# LARGE LANGUAGE MODEL COMPLETION FUNCTIONS
#
###############################################################################

function _get_system_message_prompt() {
    echo "You are a helpful zsh_completion script. \
Generate relevant and concise auto-complete suggestion 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 command must be on a new line and must not span multiple lines. \
Each must be a valid command or set of commands. \
Please focus on the user's intent, recent commands, and the current environment when \
brainstorming completions. \
The output must not contain any backticks or quotes such as \`command\` or \"command\".
"
}

function _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 here
DO NOT wrap the commands in backticks or quotes such as \`command\` or \"command\" or \`\`\`command\`\`\`
Each must be a valid command or set of commands somehow chained together that could be run in the terminal
Please focus on the user's intent, recent commands, and the current environment when brainstorming completions.
Take a deep breath. You got this!
RETURN A JSON OBJECT WITH THE COMPLETIONS"
}

function _get_command_history() {
    local HISTORY_LIMIT
    HISTORY_LIMIT=${ACSH_MAX_HISTORY_COMMANDS:-20}
    fc -l -n -$HISTORY_LIMIT
}

# Find and replace sensitive information in the command history
function _get_clean_command_history() {
    local recent_history
    recent_history=$(_get_command_history)
    recent_history=$(echo "$recent_history" | sed -E 's/:[[:xdigit:]]{36,40}/REDACTED_HASH/' )
    recent_history="${recent_history//[A-Za-z0-9-]{36}/REDACTED_UUID}"
    recent_history="${recent_history//[A-Za-z0-9]{16,40}/REDACTED_APIKEY}"
    echo -e "$recent_history"
}

function _get_recent_files() {
    local FILE_LIMIT
    FILE_LIMIT=${ACSH_MAX_RECENT_FILES:-20}
    find . -maxdepth 1 -type f -exec ls -ld {} + | sort -r | head -n "$FILE_LIMIT"
}

# Attempts to get the help message for a given command
function _get_help_message() {
    local COMMAND HELP_INFO
    COMMAND=$(echo "$1" | awk '{print $1}')

    HELP_INFO=""
    {
        HELP_INFO=$(
            cat <<EOF
    $($COMMAND --help 2>&1 || true)
EOF
        )
    } || HELP_INFO="\`$COMMAND --help\` not available"
    echo "$HELP_INFO"
}

# Constructs a LLM prompt with the user input and in-terminal contextual information
function _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|ZSH|USER|HOME|TERM|OLDPWD|HOST')

    prompt="User command: \`$user_input\`

# Terminal Context
## Environment variables
$terminal_context

Other defined environment variables
\`\`\`
$other_environment_variables
\`\`\`

## History
Recently run commands (in order) - Note some information is REDACTED
\`\`\`
$command_history
\`\`\`

## File system
Most recently modified files in the current directory:
\`\`\`
$recent_files
\`\`\`

## Help Information
$help_message

# Instructions
$output_instructions
"
    echo "$prompt"
}

# Constructs the payload for the API request
function _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)

    # EXPORT PROMPT TO #ACSH_PROMPT
    acsh_prompt="# SYSTEM PROMPT\n"
    acsh_prompt+=$system_message_prompt
    acsh_prompt+="\n# USER MESSAGE\n"
    acsh_prompt+=$prompt
    export ACSH_PROMPT=$acsh_prompt

    case ${ACSH_PROVIDER:l} in
        anthropic)
            payload=$(jq -cn --arg model "$model" --argjson temperature "$temperature" --arg system_prompt "$system_message_prompt" --arg prompt_content "$prompt" '{
            model: $model,
            system: $system_prompt,
            messages: [
                {role: "user", content: $prompt_content}
            ],
            temperature: $temperature,
            "max_tokens": 1024,
            tool_choice: {"type": "tool", "name": "zsh_completions"},
            tools:[
                {
                    "name": "zsh_completions",
                    "description": "syntactically correct command-line suggestions based on the users input",
                    "input_schema": {
                        "type": "object",
                        "properties": {
                            "commands": {
                                "type": "array",
                                "items": {
                                    "type": "string",
                                    "description": "A suggested command"
                                }
                            },
                        },
                        "required": ["commands"],
                    },
                }
            ]
            }')
            ;;
        groq)
            payload=$(jq -cn --arg model "$model" --argjson temperature "$temperature" --arg system_prompt "$system_message_prompt" --arg prompt_content "$prompt" '{
                model: $model,
                messages: [
                    {role: "system", content: $system_prompt},
                    {role: "user", content: $prompt_content}
                ],
                temperature: $temperature,
                response_format: { "type": "json_object" }
            }')
            ;;
        ollama)
            payload=$(jq -cn --arg model "$model" --argjson temperature "$temperature" --arg system_prompt "$system_message_prompt" --arg prompt_content "$prompt" '{
                model: $model,
                system: $system_prompt,
                messages: [
                    {role: "user", content: $prompt_content}
                ],
                "format": "json",
                stream: false,
                "options": {
                    "temperature": $temperature,
                }
            }')
            ;;
        *)
            payload=$(jq -cn --arg model "$model" --argjson temperature "$temperature" --arg system_prompt "$system_message_prompt" --arg prompt_content "$prompt" '{
                model: $model,
                messages: [
                    {role: "system", content: $system_prompt},
                    {role: "user", content: $prompt_content}
                ],
                temperature: $temperature,
                response_format: { "type": "json_object" },
                tool_choice: {"type": "function", "function": {"name": "zsh_completions"}},
                tools:[
                {
                    "type": "function",
                    "function": {
                        "name": "zsh_completions",
                        "description": "syntactically correct command-line suggestions based on the users input",
                        "parameters": {
                            "type": "object",
                            "properties": {
                                "commands": {
                                    "type": "array",
                                    "items": {
                                        "type": "string",
                                        "description": "A suggested command"
                                    }
                                },
                            },
                            "required": ["commands"],
                        },
                    },
                }
            ]
            }')
            ;;
    esac
    echo "$payload"
}

function log_request() {
    local user_input response_body user_input_hash log_file
    local prompt_tokens prompt_tokens_int completion_tokens completion_tokens_int created api_cost

    user_input="$1"
    response_body="$2"

    user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1)

    if [[ "${ACSH_PROVIDER:l}" == "anthropic" ]]; then
        prompt_tokens=$(echo "$response_body" | jq -r '.usage.input_tokens')
        prompt_tokens_int=$((prompt_tokens))
        completion_tokens=$(echo "$response_body" | jq -r '.usage.output_tokens')
        completion_tokens_int=$((completion_tokens))
    else
        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))
    fi

    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/.autocomplete/autocomplete.log"}
    echo "$created,$user_input_hash,$prompt_tokens_int,$completion_tokens_int,$api_cost" >> "$log_file"
}

function openai_completion() {
    local content status_code response_body default_user_input user_input
    local api_key config_file payload response completions endpoint timeout
    local prompt_tokens completion_tokens created api_cost prompt_tokens_int completion_tokens_int

    endpoint=${ACSH_ENDPOINT:-"https://api.openai.com/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:u} != "OLLAMA" ]]; then
        echo ""
        echo_error "ACSH_ACTIVE_API_KEY not set"
        echo -e "Please set it using the following command: \e[90mexport ${ACSH_PROVIDER:u}_API_KEY=<your-api-key>\e[0m"
        echo -e "or set it in the ~/.autocomplete/config configuration file via: \e[90mautocomplete config set ${ACSH_PROVIDER:u}_API_KEY <your-api-key>\e[0m"
        return
    fi
    api_key="${ACSH_ACTIVE_API_KEY:-$OPENAI_API_KEY}"
    payload=$(_build_payload "$user_input")

    case "${ACSH_PROVIDER:l}" in
        anthropic)
            response=$(curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \
            -H "content-type: application/json" \
            -H "anthropic-version: 2023-06-01" \
            -H "x-api-key: $api_key" \
            --data "$payload")
            ;;
        ollama)
            response=$(curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \
            --data "$payload")
            ;;
        *)
            response=$(curl -s -m "$timeout" -w "%{http_code}" "$endpoint" \
            -H "Content-Type: application/json" \
            -H "Authorization: Bearer $api_key" \
            -d "$payload")
            ;;
    esac

    status_code=$(echo "$response" | tail -n1)
    response_body=$(echo "$response" | sed '$d')

    if [[ $status_code -eq 200 ]]; then
        case "${ACSH_PROVIDER:l}" in
            anthropic)
                content=$(echo "$response_body" | jq -r '.content[0].input.commands')
                ;;
            groq)
                content=$(echo "$response_body" | jq -r '.choices[0].message.content')
                content=$(echo "$content" | jq -r '.completions')
                ;;
            ollama)
                content=$(echo "$response_body" | jq -r '.message.content')
                content=$(echo "$content" | jq -r '.completions')
                ;;
            *)
                content=$(echo "$response_body" | jq -r '.choices[0].message.tool_calls[0].function.arguments')
                content=$(echo "$content" | jq -r '.commands')
                ;;
        esac

        completions=$(echo "$content" | jq -r '.[]' | grep -v '^$')
        echo -n "$completions"

        log_request "$user_input" "$response_body"
    else
        echo
        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 from the API - $response_body \n$payload"
                ;;
        esac
    fi
}

###############################################################################
#
# Completion Functions
#
###############################################################################

function _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; } } }'
}

function _default_completion() {
    local current_word=""
    local first_word=""
    local default_func

    if [[ -n "${words[*]}" ]]; then
        first_word="${words[1]}"
        if [[ -n "$CURRENT" && "$CURRENT" -le "${#words[@]}" ]]; then
            current_word="${words[CURRENT]}"
        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
            compadd -a file_completions
        fi
    fi
}

function list_cache() {
    local cache_dir cache_files
    cache_dir=${ACSH_CACHE_DIR:-"$HOME/.autocomplete/cache"}
    cache_files=$(find "$cache_dir" -maxdepth 1 -type f -name "acsh-*" -printf '%T+ %p\n' | sort)
    echo "$cache_files"
}

function _autocompletezsh() {
    local curcontext="$curcontext" state line
    typeset -A opt_args

    _arguments \
        '1: :->command' \
        '*: :->args'

    case $state in
        command)
            local -a commands
            commands=(
                'install:Install the autocomplete script'
                'remove:Remove the autocomplete script'
                'config:Display or set configuration'
                'enable:Enable the autocomplete script'
                'disable:Disable the autocomplete script'
                'clear:Clear the cache and log'
                'usage:Display usage information'
                'system:Display system information'
                'command:Run autocomplete command'
                'model:Select language model'
            )
            _describe -t commands 'autocomplete commands' commands
            ;;
        args)
            case $words[1] in
                config)
                    _arguments '2:action:(set reset)'
                    ;;
                command)
                    _arguments '--dry-run[Show prompt without running]'
                    ;;
            esac
            ;;
    esac

    if [[ ${#compadd_args[@]} -eq 0 && $CURRENT -gt 1 ]]; then
        local completions
        completions=$(openai_completion "${words[*]}")
        if [[ -n "$completions" ]]; then
            _values 'completions' ${(f)completions}
        fi
    fi
}

###############################################################################
#
# CLI ENTRY POINT
#
###############################################################################

function show_help() {
    echo_green "Autocomplete.sh - LLM Powered Zsh Completion"
    echo "Usage: autocomplete [options] command"
    echo "       autocomplete [options] install|remove|config|model|enable|disable|clear|usage|system|command|--help"
    echo
    echo "autocomplete.sh is a script to enhance zsh completion with LLM capabilities."
    echo
    echo "Once installed and enabled, it will provide suggestions for the current command."
    echo "Just by pressing the Tab key, you can get the most likely completion for the command."
    echo "It provides various commands to manage and configure the autocomplete features."
    echo
    echo "Most used:"
    echo "  command             Run the autocomplete command same as pressing <tab><tab>"
    echo "  command --dry-run   Only show the prompt without running the command"
    echo
    echo "Configuration and Settings:"
    echo "  model               Change the language model used for completion"
    echo "  usage               Display usage information including cost"
    echo "  system              Displays system information"
    echo "  config              Displays status and config values"
    echo "  config set <key> <value>  Set a configuration value"
    echo "  config reset        Reset configuration to default values"
    echo
    echo "Installation / Removal / Cache Reset:"
    echo "  install             Install the autocomplete script from .zshrc"
    echo "  remove              Remove the autocomplete script from .zshrc"
    echo "  enable              Enable the autocomplete script"
    echo "  disable             Disable the autocomplete script"
    echo "  clear               Clear the cache directory and log file"
    echo
    echo "Submit bugs or feedback here: https://github.com/closedloop-technologies/autocomplete-sh/issues"
    echo "For more information, visit: https://autocomplete.sh"
}

# Function to check if running in a subshell
function is_subshell() {
    if [[ $$ != $PPID ]]; then
        return 0
    else
        return 1
    fi
}

function show_config() {
    local config_file config_value
    local term_width small_table rest

    echo_green "Autocomplete.sh - Configuration and Settings - Version $ACSH_VERSION"

    if is_subshell; then
        echo -e "  STATUS: \033[33;5mUnknown\033[0m \033[0m"
        echo -e "  Run \033[33;5msource autocomplete config\033[0m to see if autocomplete is enabled"
        return
    elif check_if_enabled; then
        echo -e "  STATUS: \033[32;5mEnabled\033[0m \033[0m"
    else
        echo -e "  STATUS: \033[31;5mDisabled\033[0m \033[0m- WARNING might be a false negative.  Make sure to"
        echo -e "  run \033[33;5msource autocomplete config\033[0m to confirm if autocomplete disabled"
    fi
    config_file="$HOME/.autocomplete/config"
    if [ ! -f "$config_file" ]; then
        echo_error "Configuration file not found: $config_file"
        echo_error "Run autocomplete install"
        return
    fi
    acsh_load_config
    echo
    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 ${(k)parameters[(R)*ACSH_*]}; do
        if [[ $config_var == "ACSH_INPUT" ]] || [[ $config_var == "ACSH_PROMPT" ]] || [[ $config_var == "ACSH_RESPONSE" ]]; then
            continue
        fi
        config_value="${(P)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 ${(k)parameters[(R)*ACSH_*]}; do
        if [[ $config_var == "ACSH_INPUT" ]] || [[ $config_var == "ACSH_PROMPT" ]] || [[ $config_var == "ACSH_RESPONSE" ]]; then
            continue
        fi
        config_value="${(P)config_var}"
        if [[ ${config_var: -8} != "_API_KEY" ]]; then
            continue
        fi
        echo -en "  $config_var:\e[90m"
        if [[ -z ${(P)config_var} ]]; then
            echo -en "\e[31m"
            config_value="UNSET"
        else
            echo -en "\e[32m"
            rest=${(P)config_var:4}
            config_value="${(P)config_var:0:4}...${rest: -4}"
        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
}

function set_config() {
    local key="$1"
    local value="$2"
    local config_file="$HOME/.autocomplete/config"

    key=${key:l}
    key=${key//[^a-zA-Z0-9]/_}

    if [ -z "$key" ]; then
        echo_error "SyntaxError: expected \`autocomplete config set <key> <value>\`"
        return
    fi

    if [ ! -f "$config_file" ]; then
        echo_error "Configuration file not found: $config_file"
        echo_error "Run autocomplete install"
        return
    fi

    sed -i "s|^\($key:\).*|\1 $value|" "$config_file"

    acsh_load_config
}

function config_command() {
    local command config_file

    config_file="$HOME/.autocomplete/config"
    command="${*:2}"

    if [ -z "$command" ]; then
        show_config
        return
    fi
    if [ "$2" = "set" ]; then
        local key="$3"
        local value="$4"
        echo -e "Setting configuration key \`$key\` to value \`$value\`"
        set_config "$key" "$value"
        echo_green "Configuration updated: run \`autocomplete config\` to see the 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 \`autocomplete config set <key> <value>\ or autocomplete config reset\`"
}

function build_config() {
    local config_file default_config api_key
    config_file="$HOME/.autocomplete/config"

    if [ ! -f "$config_file" ]; then
        echo "Creating the ~/.autocomplete/config file with default values"
        api_key="${ACSH_ACTIVE_API_KEY:-$OPENAI_API_KEY}"
        default_config="# ~/.autocomplete/config

# OpenAI API Key set here or as an environment variable named OPENAI_API_KEY
# https://platform.openai.com/api-keys
openai_api_key: $OPENAI_API_KEY

# Anthropic/Claude API Key set here or as an environment variable ANTHROPIC_API_KEY
# https://console.anthropic.com/settings/keys
anthropic_api_key: $ANTHROPIC_API_KEY

# Groq API Key set here or as en environment variable GROQ_API_KEY
# https://console.groq.com/keys
groq_api_key: $GROQ_API_KEY

# If your custom deployment like Ollama needs a key set it here or LLM_API_KEY
custom_api_key: $LLM_API_KEY

# Model configuration
## Model List https://platform.openai.com/docs/models
provider: openai
model: gpt-4o
temperature: 0.0
endpoint: https://api.openai.com/v1/chat/completions
# If pricing is outdated, submit issue: https://github.com/closedloop-technologies/autocomplete-sh/issues
# Sources: https://openai.com/api/pricing/
api_prompt_cost: 0.000005
api_completion_cost: 0.000015

# Max number of history commands and recent files to include in the prompt
max_history_commands: 20
max_recent_files: 20

# Cache settings
cache_dir: $HOME/.autocomplete/cache
cache_size: 10

# Logging settings
log_file: $HOME/.autocomplete/autocomplete.log"

        echo "$default_config" > "$config_file"
    fi
}

function acsh_load_config() {
    local config_file key value

    config_file="$HOME/.autocomplete/config"

    if [ -f "$config_file" ]; then
        while IFS=':' read -r key value; do
            if [[ $key == \#* ]] || [[ -z $key ]]; then
                continue
            fi

            key=$(echo "$key" | tr -d '[:space:]')
            value=$(echo "$value" | tr -d '[:space:]')

            key=${key:u}
            key=${key//[^[:alnum:]]/_}

            if [[ -n $value ]]; then
                export "ACSH_$key"="$value"
            fi
        done < "$config_file"

        if [[ -z "$ACSH_OPENAI_API_KEY" && -n "$OPENAI_API_KEY" ]]; then
            export ACSH_OPENAI_API_KEY="$OPENAI_API_KEY"
        fi
        if [[ -z "$ACSH_ANTHROPIC_API_KEY" && -n "$ANTHROPIC_API_KEY" ]]; then
            export ACSH_ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
        fi
        if [[ -z "$ACSH_GROQ_API_KEY" && -n "$GROQ_API_KEY" ]]; then
            export ACSH_GROQ_API_KEY="$GROQ_API_KEY"
        fi
        if [[ -z "$ACSH_CUSTOM_API_KEY" && -n "$LLM_API_KEY" ]]; then
            export ACSH_CUSTOM_API_KEY="$LLM_API_KEY"
        fi

        case "${ACSH_PROVIDER:-openai}" in
            "openai")
                export ACSH_ACTIVE_API_KEY="$ACSH_OPENAI_API_KEY"
                ;;
            "anthropic")
                export ACSH_ACTIVE_API_KEY="$ACSH_ANTHROPIC_API_KEY"
                ;;
            "groq")
                export ACSH_ACTIVE_API_KEY="$ACSH_GROQ_API_KEY"
                ;;
            "ollama")
                export ACSH_ACTIVE_API_KEY="$ACSH_OLLAMA_API_KEY"
                ;;
            *)
                echo_error "Unknown provider: $ACSH_PROVIDER"
                ;;
        esac
    else
        echo "Configuration file not found: $config_file"
    fi
}

function install_command() {
    local zshrc_file autocomplete_setup autocomplete_cli_setup

    zshrc_file="$HOME/.zshrc"
    autocomplete_setup="source autocomplete enable"
    autocomplete_cli_setup="compdef _autocompletezsh autocomplete"

    if ! command -v autocomplete &>/dev/null; then
        echo_error "autocomplete.sh is not in the PATH
Please follow the install instructions on https://github.com/closedloop-technologies/autocomplete-sh"
        return
    fi

    if [[ ! -d "$HOME/.autocomplete" ]]; then
        echo "Creating the ~/.autocomplete directory"
        mkdir -p "$HOME/.autocomplete"
    fi

    local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.autocomplete/cache"}
    if [[ ! -d "$cache_dir" ]]; then
        mkdir -p "$cache_dir"
    fi

    build_config
    acsh_load_config

    if ! grep -qF "$autocomplete_setup" "$zshrc_file"; then
        echo -e "# Autocomplete.sh" >> "$zshrc_file"
        echo -e "$autocomplete_setup\n" >> "$zshrc_file"
        echo "Added autocomplete.sh setup to $zshrc_file"
    else
        echo "Autocomplete.sh setup already exists in $zshrc_file"
    fi

    if ! grep -qF "$autocomplete_cli_setup" "$zshrc_file"; then
        echo -e "# Autocomplete.sh CLI" >> "$zshrc_file"
        echo -e "$autocomplete_cli_setup\n" >> "$zshrc_file"
        echo "Added autocomplete cli autocomplete to $zshrc_file"
    fi

    echo
    echo_green "Autocomplete.sh - LLM Powered Zsh Completion - Version $ACSH_VERSION"
    echo -e "\e[1;34mInstall Completed\e[0m"
    echo
    echo "Two more steps to complete the installation:"
    echo -e "1. $ \e[1;34msource $zshrc_file\e[0m to enable autocomplete"
    echo -e "2. $ \e[1;34mautocomplete model\e[0m \tto select the language model"
    echo
    echo -e "$ \e[1;30mautocomplete config\e[0m \tto view settings"
    echo -e "$ \e[1;30mautocomplete --help\e[0m \tfor additional commands"
    echo
    echo -e "Visit \e[1;32mhttps://autocomplete.sh\e[0m for examples and more information"
    echo
}

function remove_command() {
    local config_file cache_dir log_file zshrc_file

    config_file="$HOME/.autocomplete/config"
    cache_dir=${ACSH_CACHE_DIR:-"$HOME/.autocomplete/cache"}
    log_file=${ACSH_LOG_FILE:-"$HOME/.autocomplete/autocomplete.log"}
    zshrc_file="$HOME/.zshrc"

    echo_green "Autocomplete.sh - Removing files, directories, and zshrc setup..."

    if [ -f "$config_file" ]; then
        rm "$config_file"
        echo "Removed: $config_file"
    fi

    if [ -d "$cache_dir" ]; then
        rm -rf "$cache_dir"
        echo "Removed: $cache_dir"
    fi

    if [ -f "$log_file" ]; then
        rm "$log_file"
        echo "Removed: $log_file"
    fi

    if [ -d "$HOME/.autocomplete" ]; then
        if [ -z "$(ls -A "$HOME/.autocomplete")" ]; then
            rmdir "$HOME/.autocomplete"
            echo "Removed: $HOME/.autocomplete"
        else
            echo "Skipped removing $HOME/.autocomplete (directory not empty)"
        fi
    fi

    if [ -f "$zshrc_file" ]; then
        if grep -qF "source autocomplete enable" "$zshrc_file"; then
            sed -i '/# Autocomplete.sh/d' "$zshrc_file"
            sed -i '/autocomplete/d' "$zshrc_file"
            echo "Removed autocomplete.sh setup from $zshrc_file"
        fi
    fi

    local autocomplete_script
    autocomplete_script=$(command -v autocomplete)
    if [ -n "$autocomplete_script" ]; then
        echo "The autocomplete script is located at: $autocomplete_script"

        if [ "$1" = "-y" ]; then
            rm "$autocomplete_script"
            echo "Removed: $autocomplete_script"
        else
            read -r "confirm?Do you want to remove the autocomplete script? (y/n): "
            if [[ $confirm == "y" ]]; then
                rm "$autocomplete_script"
                echo "Removed: $autocomplete_script"
            fi
        fi
    fi

    echo "Completed uninstalling autocomplete.sh"
}

function check_if_enabled() {
    local is_enabled
    is_enabled=$(complete -p | grep _autocompletezsh | grep -cv _autocompletezsh_cli)
    if [ "$is_enabled" -gt 0 ]; then
        return 0
    else
        return 1
    fi
}

function enable_command() {
    if check_if_enabled; then
        echo_green "Autocomplete.sh - reloading"
        disable_command
    fi
    acsh_load_config
    compdef _autocompletezsh -D -E
}

function disable_command() {
    if check_if_enabled; then
        uncompdef -D
    fi
}

function command_command() {
    local args=("$@")
    for ((i = 1; i <= ${#args[@]}; i++)); do
        if [ "${args[i]}" = "--dry-run" ]; then
            args[i]=()
            _build_prompt "${args[@]}"
            return
        fi
    done
    openai_completion "$@" || true
    echo
}

function clear_command() {
    local cache_dir log_file
    acsh_load_config
    cache_dir=${ACSH_CACHE_DIR:-"$HOME/.autocomplete/cache"}
    log_file=${ACSH_LOG_FILE:-"$HOME/.autocomplete/autocomplete.log"}

    echo "This will remove the cache directory and log file"
    echo -e "Cache dir:\t\e[31m$cache_dir\e[0m"
    echo -e "Log file:\t\e[31m$log_file\e[0m"
    read -r "confirm?Are you sure you want to continue? (y/n): "
    if [[ $confirm != "y" ]]; then
        echo "Aborted"
        return
    fi
    if [ -d "$cache_dir" ]; then
        cache_files=$(list_cache)
        echo "$cache_file"
        if [ -n "$cache_files" ]; then
            for last_update_and_filename in $cache_files; do
                file=$(echo "$last_update_and_filename" | cut -d ' ' -f 2)
                rm "$file"
                echo "Removed: $file"
            done
            echo "Removed files in: $cache_dir"
        else
            echo "Cache directory is empty"
        fi

        echo "Removed: $cache_dir"
    fi
    if [ -f "$log_file" ]; then
        rm "$log_file"
        echo "Removed: $log_file"
    fi
}

function usage_command() {
    local log_file number_of_lines api_cost cache_dir
    log_file=${ACSH_LOG_FILE:-"$HOME/.autocomplete/autocomplete.log"}
    cache_dir=${ACSH_CACHE_DIR:-"$HOME/.autocomplete/cache"}

    cache_size=$(list_cache | wc -l)

    echo_green "Autocomplete.sh - Usage Information"
    echo
    echo -n "Log file: "
    echo -en "\e[90m"
    echo "$log_file"
    echo -en "\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 "API Calls:\t"

    if [[ $number_of_lines -eq 0 ]]; then
        echo_error "No usage data found"
        return
    else
        earliest_date=$(awk -F, '{print $1}' "$log_file" | sort | head -n 1)
        if [[ -n "$earliest_date" ]]; then
            earliest_date=$(date -d@"$earliest_date")
            echo -en "\e[90m"
            echo "Since $earliest_date"
            echo -en "\e[0m"
        fi
    fi
    echo
    echo -en "\tUsage count:\t"
    echo -en "\e[32m"
    printf "%9s\n" "$number_of_lines"
    echo -en "\e[0m"
    echo -en "\tAvg Cost:\t$"
    printf "%8.4f\n" "$avg_api_cost"
    echo -e "\e[90m\t-------------------------\e[0m"
    echo -en "\tTotal Cost:\t$"
    echo -en "\e[31m"
    printf "%8.4f\n" "$api_cost"
    echo -en "\e[0m"
    echo
    echo
    echo -n "Cache Size: ${cache_size} of ${ACSH_CACHE_SIZE:-10} in "
    echo -e "\e[90m$cache_dir\e[0m"
    echo
    echo -e "To clear the log file and cache directory, run: \e[90mautocomplete clear\e[0m"
}

function get_key() {
    local key
    read -sk1 key
    case $key in
        $'\x1b')
            read -sk2 key
            case $key in
                '[A') echo up ;;
                '[B') echo down ;;
            esac
            ;;
        '') echo enter ;;
    esac
}

function menu_selector() {
    local -a options
    options=("$@")
    local selected=1

    function show_menu() {
        echo
        echo "Select a Language Model:"
        echo
        for i in {1..${#options[@]}}; do
            if [[ $i -eq $selected ]]; then
                echo -e "\e[1;32m> ${options[i]}\e[0m"
            else
                echo -e "  ${options[i]}"
            fi
        done
        echo
        echo -ne "Press Enter to select the model\t"
    }

    tput sc

    while true; do
        tput rc
        tput ed
        show_menu
        key=$(get_key)
        case $key in
            up)
                ((selected--))
                if ((selected < 1)); then
                    selected=${#options[@]}
                fi
                ;;
            down)
                ((selected++))
                if ((selected > ${#options[@]})); then
                    selected=1
                fi
                ;;
            enter)
                break
                ;;
        esac
    done
    clear
    return $((selected - 1))
}

function model_command() {
    clear
    local selected_model options=()

    if [[ $# -ne 3 ]]; then
        local -a sorted_keys
        sorted_keys=(${(onk)_autocomplete_modellist})

        for key in $sorted_keys; do
            options+=("$key")
        done

        echo -e "\e[1;32mAutocomplete.sh - Model Configuration\e[0m"
        menu_selector "${options[@]}"
        selected_option=$?
        echo -e "\e[1;32mAutocomplete.sh - Model Configuration\e[0m"
        selected_model="${options[selected_option+1]}"
        selected_value="${_autocomplete_modellist[$selected_model]}"
    else
        local provider="$2"
        local model_name="$3"
        selected_value="${_autocomplete_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')"

    local prompt_cost=$(echo "$selected_value" | jq -r '.prompt_cost' | awk '{printf "%.8f", $1}' )
    local 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"

    if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER:u} != "OLLAMA" ]]; then
        echo -e "\e[34mSet ${ACSH_PROVIDER:u}_API_KEY\e[0m"
        echo
        echo -e "This is stored locally here: \e[90m~/.autocomplete/config\e[0m"
        echo
        case ${ACSH_PROVIDER:u} in
            OPENAI)
                echo "Create a new one here: https://platform.openai.com/settings/profile?tab=api-keys"
                ;;
            ANTHROPIC)
                echo "Create a new one here: https://console.anthropic.com/settings/keys"
                ;;
            GROQ)
                echo "Create a new one here: https://console.groq.com/keys"
                ;;
        esac
        echo
        echo -n "Enter your ${ACSH_PROVIDER:u} API Key: "
        read -rs user_api_key_input
        clear
        echo -e "\e[1;32mAutocomplete.sh - Model Configuration\e[0m"
        if [[ -n "$user_api_key_input" ]]; then
            export ACSH_ACTIVE_API_KEY="$user_api_key_input"
            set_config "${ACSH_PROVIDER:l}_api_key" "$user_api_key_input"
        fi
    fi

    local model="${ACSH_MODEL:-"ERROR"}"
    local 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:    \t\$$ACSH_API_PROMPT_COST\e[0m"
    echo -e "            \t\e[90mcompletion:\t\$$ACSH_API_COMPLETION_COST\e[0m"
    echo
    echo -e "Endpoint:   \t\e[90m$ACSH_ENDPOINT\e[0m"
    echo -en "API Key:    \t"

    if [[ -z $ACSH_ACTIVE_API_KEY ]]; then
        if [[ ${ACSH_PROVIDER:u} == "OLLAMA" ]]; then
            echo -e "\e[90mNot Used\e[0m"
        else
            echo -e "\e[31mUNSET"
        fi
    else
        local rest=${ACSH_ACTIVE_API_KEY:4}
        local config_value="${ACSH_ACTIVE_API_KEY:0:4}...${rest: -4}"
        echo -en "\e[32m${config_value}"
    fi
    echo -en "\e[0m"

    if [[ -z $ACSH_ACTIVE_API_KEY && ${ACSH_PROVIDER:u} != "OLLAMA" ]]; then
        echo
        echo "To set the API Key, run either:"
        echo -e "\t\e[31mautocomplete config set api_key <your-api-key>\e[0m"
        echo -e "\t\e[31mexport ${ACSH_PROVIDER:u}_API_KEY=<your-api-key>\e[0m"
    else
        echo
    fi

    if [[ ${ACSH_PROVIDER:u} == "OLLAMA" ]]; then
        echo "To set a custom endpoint:"
        echo -e "\t\e[34mautocomplete config set endpoint <your-url>\e[0m"
        echo "Other models can be set:"
        echo -e "\t\e[34mautocomplete config set model <model-name>\e[0m"
    fi
    echo "To change the temperature:"
    echo -e "\t\e[90mautocomplete config set temperature <temperature>\e[0m"
    echo
}

# Main script logic
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 \`autocomplete --help\` or goto https://autocomplete.sh for more information"
        else
            echo_green "Autocomplete.sh - LLM Powered Zsh Completion - Version $ACSH_VERSION - https://autocomplete.sh"
        fi
        ;;
esac

# Enable completion for the autocomplete command itself
compdef _autocompletezsh autocomplete
bobbravo2 commented 1 month ago

@closedLoop running in zsh userland over here, so I may be able to help, a bit.

On OSX, one of the friction points I'm seeing with v0.4.1 is when I copy/paste the install.sh, I get the following error:

wget -qO- https://autocomplete.sh/install.sh | bash 2024-08-04 10:55:51 URL:https://raw.githubusercontent.com/closedloop-technologies/autocomplete- sh/v0.4.1/autocomplete.sh [51027/51027] -> "/Users/localhost/.local/bin/autocomplete" [1] ERROR: Please ensure you have bash-completion installed and sourced.

[I belive/hope] Relevant lines of .zshrc:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 

As well, when I run autocomplete directly from zsh, I get this output:

/Users/localhost/.local/bin/autocomplete: line 20: declare: -A: invalid option
declare: usage: declare [-afFirtx] [-p] [name[=value] ...]
/Users/localhost/.local/bin/autocomplete: line 23: openai:\tgpt-4o: syntax error in expression (error token is ":\tgpt-4o")
/Users/localhost/.local/bin/autocomplete: line 24: openai:\tgpt-4o-mini: syntax error in expression (error token is ":\tgpt-4o-mini")
/Users/localhost/.local/bin/autocomplete: line 25: openai:\tgpt-3.5-turbo-0125: syntax error in expression (error token is ":\tgpt-3.5-turbo-0125")
/Users/localhost/.local/bin/autocomplete: line 27: anthropic:\tclaude-3-5-sonnet-20240620: syntax error in expression (error token is ":\tclaude-3-5-sonnet-20240620")
/Users/localhost/.local/bin/autocomplete: line 28: anthropic:\tclaude-3-opus-20240229: syntax error in expression (error token is ":\tclaude-3-opus-20240229")
/Users/localhost/.local/bin/autocomplete: line 29: anthropic:\tclaude-3-haiku-20240307: syntax error in expression (error token is ":\tclaude-3-haiku-20240307")
/Users/localhost/.local/bin/autocomplete: line 32: groq:\t\tllama3-8b-8192: syntax error in expression (error token is ":\t\tllama3-8b-8192")
/Users/localhost/.local/bin/autocomplete: line 33: groq:\t\tllama3-70b-8192: syntax error in expression (error token is ":\t\tllama3-70b-8192")
/Users/localhost/.local/bin/autocomplete: line 34: groq:\t\tmixtral-8x7b-32768: syntax error in expression (error token is ":\t\tmixtral-8x7b-32768")
/Users/localhost/.local/bin/autocomplete: line 35: groq:\t\tgemma-7b-it: syntax error in expression (error token is ":\t\tgemma-7b-it")
/Users/localhost/.local/bin/autocomplete: line 36: groq:\t\tgemma2-9b-it: syntax error in expression (error token is ":\t\tgemma2-9b-it")
/Users/localhost/.local/bin/autocomplete: line 39: ollama:\tcodellama: syntax error in expression (error token is ":\tcodellama")
\e[32mAutocomplete.sh - LLM Powered Bash Completion - Version 0.4.0 - https://autocomplete.sh\e[0m

Which suggests that I may have an older version configured/already installed, and need to make updates to my zshrc for the upgrade?

ssuukk commented 1 month ago

I'm also interested in zsh support, so let mem bump this topic.

SynapticSage commented 1 week ago

Bump! Also interested.