├── .gitignore ├── LICENSE.txt ├── README.md ├── bai.sh ├── install.sh └── tools ├── cat.sh ├── find-wild.sh └── ls.sh /.gitignore: -------------------------------------------------------------------------------- 1 | dev/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | You should have received a copy of the GNU General Public License 10 | along with this program. If not, see . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bash AI 2 | 3 | Bash AI _(bai)_ is an advanced Bash shell script functioning as an AI-powered terminal assistant, inspired by [Your AI](https://github.com/ekkinox/yai).\ 4 | Leveraging the newest OpenAI's capabilities, it allows you to ask questions and perform terminal-based tasks using natural language. It provides answers and command suggestions based on your input and allows you to execute or edit the suggested commands if desired. 5 | 6 | Bash AI is not only powerful out of the box, but also expandable!\ 7 | With its plugin architecture, you can easily add your own tools, thereby empowering Bash AI to accomplish even more, and extending its functionality beyond its original capabilities. 8 | 9 | ## Features 10 | 11 | Bash AI offers the following features: 12 | 13 | - **100% Shell Script**\ 14 | No need to install anything. Just run it! 15 | 16 | - **Plugins!**\ 17 | Extend Bash AI's functionality by adding plugins known as "tools". 18 | 19 | - **Natural Language Interface**\ 20 | Communicate with the terminal using everyday language. 21 | 22 | - **Question Answering**\ 23 | Get answers to all your terminal questions by ending your request with a question mark. 24 | 25 | - **Command Suggestions**\ 26 | Receive intelligent command suggestions based on your input. 27 | 28 | - **Command Information**\ 29 | Get detailed information about the suggested commands. 30 | 31 | - **Distribution Awareness**\ 32 | Get answers and commands that are compatible with, and related to, your specific Linux distribution. 33 | 34 | - **Command Execution**\ 35 | Choose to execute the suggested commands directly from Bash AI. 36 | 37 | - **Command Editing**\ 38 | Edit the suggested commands before execution. 39 | 40 | - **Error Examination**\ 41 | Examine the error messages generated by the suggested commands and attempt to fix them. 42 | 43 | - **Persistent Memory**\ 44 | Remembers your previous requests and uses them to improve future suggestions. 45 | 46 | - **Directory Awareness**\ 47 | Automatically detects and uses the current directory when executing commands. 48 | 49 | - **Locale Awareness**\ 50 | Automatically detects your system's locale and uses it to provide localized responses. 51 | 52 | - **Vim Awareness**\ 53 | Automatically detects if you are using Vim and provides Vim-specific suggestions. 54 | 55 | ## Setup 56 | 57 | 1. To setup Bash AI quickly, you can run the following command: 58 | 59 | ```bash 60 | curl -sS https://raw.githubusercontent.com/hezkore/bash-ai/main/install.sh | bash 61 | ``` 62 | 63 | > [!WARNING] 64 | > Never run unknown scripts without reviewing them for safety. Read the install script [here](https://raw.githubusercontent.com/hezkore/bash-ai/main/install.sh). 65 | 66 | 2. Run `bai` to start Bash AI. 67 | 68 |
69 | Manual Setup 70 | 71 | 1. Clone or download the repository: 72 | 73 | ```bash 74 | git clone https://github.com/hezkore/bash-ai.git 75 | ``` 76 | 2. Make the script executable: 77 | 78 | ```bash 79 | chmod +x bai.sh 80 | ``` 81 | 82 | 3. Execute Bash AI: 83 | 84 | ```bash 85 | ./bai.sh 86 | ``` 87 | 88 | * _(Optional)_ For convenience, you can create a shortcut to the `bai.sh` script. There are two ways to do this: 89 | 90 | * Create a symbolic link in `/usr/local/bin`. This will allow you to run the script from anywhere, without having to type the full path. Replace `path/to/bai.sh` with the actual path to the `bai.sh` script: 91 | 92 | ```bash 93 | ln -s path/to/bai.sh /usr/local/bin/bai 94 | ``` 95 | 96 | * Alternatively, you can create an alias for the `bai.sh` script in your `.bashrc` file. This will also allow you to execute the script using the `bai` command, reducing the need for typing the full path to the script each time. Replace `path/to/bai.sh` with the actual path to the `bai.sh` script: 97 | 98 | ```conf 99 | alias bai='path/to/bai.sh' 100 | ``` 101 | 102 |
103 | 104 | ## Configuration 105 | 106 | On the first run, a configuration file named `bai.cfg` will be created in your `~/.config` directory. 107 | 108 | > [!IMPORTANT] 109 | > Always remove `bai.cfg` before updating Bash AI to avoid compatibility issues. 110 | 111 | You must provide a [OpenAI API key](https://platform.openai.com/api-keys) in the `key=` field of this file. The [OpenAI API key](https://platform.openai.com/api-keys) can be obtained from your [OpenAI account](https://platform.openai.com/api-keys). 112 | 113 | > [!CAUTION] 114 | > Keeping the key in a plain text file is dangerous, and it is your responsibility to keep it secure. 115 | 116 | You can also change the [GPT model](https://platform.openai.com/docs/models), [temperature](https://platform.openai.com/docs/api-reference/chat/create#chat-create-temperature) and many other things in this file. 117 | 118 | ## Usage 119 | 120 | Bash AI operates in two modes: Interactive Mode and Command Mode. 121 | 122 | To enter Interactive Mode, you simply run `bai` without any request. This allows you to continuously interact with Bash AI without needing to re-run the command. 123 | 124 | In Command Mode, you run `bai` followed by your request, like so: `bai your request here` 125 | 126 | Example usage: 127 | 128 | ``` 129 | bai create a new directory with a name of your choice, then create a text file inside it 130 | ``` 131 | 132 | You can also ask questions by ending your request with a question mark: 133 | ``` 134 | bai what is the current time? 135 | ``` 136 | 137 | ## Plugins and tools 138 | 139 | Plugins are OpenAI tools that expand Bash AI's functionality, but they are not included in the default Bash AI setup.\ 140 | All tools should be placed in your `~/.bai_tools` directory.\ 141 | You can see which tools are currently installed by running `bai`, and Bash AI will list them for you. 142 | 143 | Tools are nothing more than a shell script with a `init` and `execute` function.\ 144 | You can find examples and available tools in the [tools folder](https://github.com/Hezkore/bash-ai/tree/main/tools).\ 145 | Feel free to move them to your `~/.bai_tools` directory to enable them! 146 | 147 | ## Known Issues 148 | 149 | - In Command Mode, avoid using single quotes in your requests.\ 150 | For instance, the command `bai what's the current time?` will not work. However, both `bai "what's the current time?"` and `bai what is the current time?` will execute successfully.\ 151 | Please note that this issue is specific to the terminal, and does not occur in Interactive Mode. 152 | 153 | ## Prerequisites 154 | 155 | - [OpenAI account and API key](https://platform.openai.com/apps) 156 | - [curl](https://curl.se/download.html) 157 | - [jq](https://stedolan.github.io/jq/download/) -------------------------------------------------------------------------------- /bai.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- Mode: sh; coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- 3 | 4 | # Bash AI 5 | # https://github.com/Hezkore/bash-ai 6 | 7 | # Make sure required tools are installed 8 | if [ ! -x "$(command -v jq)" ]; then 9 | echo "ERROR: Bash AI requires jq to be installed." 10 | exit 1 11 | fi 12 | if [ ! -x "$(command -v curl)" ]; then 13 | echo "ERROR: Bash AI requires curl to be installed." 14 | exit 1 15 | fi 16 | 17 | # Determine the user's environment 18 | UNIX_NAME=$(uname -srp) 19 | # Attempt to fetch distro info from lsb_release or /etc/os-release 20 | if [ -x "$(command -v lsb_release)" ]; then 21 | DISTRO_INFO=$(lsb_release -ds | sed 's/^"//;s/"$//') 22 | elif [ -f "/etc/os-release" ]; then 23 | DISTRO_INFO=$(grep -oP '(?<=^PRETTY_NAME=").+(?="$)' /etc/os-release) 24 | fi 25 | # If we failed to fetch distro info, we'll mark it as unknown 26 | if [ ${#DISTRO_INFO} -le 1 ]; then 27 | DISTRO_INFO="Unknown" 28 | fi 29 | 30 | # Version of Bash AI 31 | VERSION="1.0.5" 32 | 33 | # Global variables 34 | PRE_TEXT=" " # Prefix for text output 35 | NO_REPLY_TEXT="¯\_(ツ)_/¯" # Text for no reply 36 | INTERACTIVE_INFO="Hi! Feel free to ask me anything or give me a task. Type \"exit\" when you're done." # Text for interactive mode intro 37 | PROGRESS_TEXT="Thinking..." 38 | PROGRESS_ANIM="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" 39 | HISTORY_MESSAGES="" # Placeholder for history messages, this will be updated later 40 | 41 | # Theme colors 42 | CMD_BG_COLOR="\e[48;5;236m" # Background color for cmd suggestions 43 | CMD_TEXT_COLOR="\e[38;5;203m" # Text color for cmd suggestions 44 | INFO_TEXT_COLOR="\e[90;3m" # Text color for all information messages 45 | ERROR_TEXT_COLOR="\e[91m" # Text color for cmd errors messages 46 | CANCEL_TEXT_COLOR="\e[93m" # Text color cmd for cancellation message 47 | OK_TEXT_COLOR="\e[92m" # Text color for cmd success message 48 | TITLE_TEXT_COLOR="\e[1m" # Text color for the Bash AI title 49 | 50 | # Terminal control constants 51 | CLEAR_LINE="\033[2K\r" 52 | HIDE_CURSOR="\e[?25l" 53 | SHOW_CURSOR="\e[?25h" 54 | RESET_COLOR="\e[0m" 55 | 56 | # Default query constants, these are used as default values for different types of queries 57 | DEFAULT_EXEC_QUERY="Return only a single compact JSON object containing 'cmd' and 'info' fields. 'cmd' must always contain one or multiple commands to perform the task specified in the user query. 'info' must always contain a single-line string detailing the actions 'cmd' will perform and the purpose of all command flags. 'cmd' may output a shell script to perform complex tasks. 'cmd' may be omittied as a last resort if no command can be suggested." 58 | DEFAULT_QUESTION_QUERY="Return only a single compact JSON object containing a 'info' field. 'info' must always contain a single-line string terminal-related answer to the user query." 59 | DEFAULT_ERROR_QUERY="Return only a single compact JSON object containing 'cmd' and 'info' fields. 'cmd' is optional. 'cmd' must always contain a suggestion on how to fix, solve or repair the error in the user query. 'info' must always be a single-line string explaining what the error in the user query means, why it happened, and why 'cmd' might fix it. Use your tools to find out why the error occured and offer alternatives." 60 | DYNAMIC_SYSTEM_QUERY="" # After most user queries, we'll add some dynamic system information to the query 61 | 62 | # Global query variable, this will be updated with specific user and system information 63 | GLOBAL_QUERY="You are Bash AI (bai) v${VERSION}. You are an advanced Bash shell script. You are located at \"$0\". You do not have feelings or emotions, do not convey them. Please give precise curt answers. Please do not include any sign off phrases or platitudes, only respond precisely to the user. Bash AI is made by Hezkore. You execute the tasks the user asks from you by utilizing the terminal and shell commands. No task is too big. Always assume the query is terminal and shell related. You support user plugins called \"tools\" that extends your capabilities, more info and plugins can be found on the Bash AI homepage. The Bash AI homepage is \"https://github.com/hezkore/bash-ai\". You always respond with a single JSON object containing 'cmd' and 'info' fields. We are always in the terminal. The user is using \"$UNIX_NAME\" and specifically distribution \"$DISTRO_INFO\". The users username is \"$USER\" with home \"$HOME\". You must always use LANG $LANG and LC_TIME $LC_TIME." 64 | 65 | # Configuration file path 66 | CONFIG_FILE=~/.config/bai.cfg 67 | #GLOBAL_QUERY+=" Your configuration file path \"$CONFIG_FILE\"." 68 | 69 | # Test if we're in Vim 70 | if [ -n "$VIMRUNTIME" ]; then 71 | CMD_BG_COLOR="" 72 | CMD_TEXT_COLOR="" 73 | INFO_TEXT_COLOR="" 74 | ERROR_TEXT_COLOR="" 75 | CANCEL_TEXT_COLOR="" 76 | OK_TEXT_COLOR="" 77 | TITLE_TEXT_COLOR="" 78 | CLEAR_LINE="" 79 | HIDE_CURSOR="" 80 | SHOW_CURSOR="" 81 | RESET_COLOR="" 82 | 83 | # Make sure system message reflects that we're in Vim 84 | DYNAMIC_SYSTEM_QUERY+="User is inside \"$VIM\". You are in the Vim terminal." 85 | 86 | # Use the Vim history file 87 | HISTORY_FILE=/tmp/baihistory_vim.txt 88 | else 89 | # Use the default history file 90 | HISTORY_FILE=/tmp/baihistory_com.txt 91 | fi 92 | 93 | # Update info about history file 94 | #GLOBAL_QUERY+=" Your message history file path is \"$HISTORY_FILE\"." 95 | 96 | # Tools 97 | OPENAI_TOOLS="" 98 | TOOLS_PATH=~/.bai_tools 99 | 100 | # Create the directory only if it doesn't exist 101 | if [ ! -d "$TOOLS_PATH" ]; then 102 | mkdir -p "$TOOLS_PATH" 103 | fi 104 | echo "" > /tmp/bai_tool_output.txt 105 | 106 | # Declare an associative array to store function names and script paths 107 | declare -A TOOL_MAP 108 | 109 | # Iterate over all files in the tools directory 110 | for tool in "$TOOLS_PATH"/*.sh 111 | do 112 | # Check if the file exists before sourcing it 113 | if [ -f "$tool" ]; then 114 | # For each file, run it in a subshell and call its `init` function 115 | init_output=$(source "$tool"; init 2>/dev/null) 116 | 117 | # Check the exit status of the last command 118 | if [ $? -ne 0 ]; then 119 | echo "WARNING: $tool does not contain an init function." 120 | else 121 | # Test if the output is a valid JSON and pretty-print it 122 | pretty_json=$(echo "$init_output" | jq . 2>/dev/null) 123 | 124 | if [ $? -ne 0 ]; then 125 | echo "ERROR: $tool init function has JSON syntax errors." 126 | exit 1 127 | else 128 | # Extract the type from the JSON 129 | type=$(echo "$pretty_json" | jq -r '.type') 130 | 131 | # If the type is "function", extract the function name and store it in the array 132 | if [ "$type" = "function" ]; then 133 | # Extract the function name from the JSON. 134 | function_name=$(echo "$pretty_json" | jq -r '.function.name') 135 | 136 | # Check if the function name already exists in the map 137 | if [ -n "${TOOL_MAP[$function_name]}" ]; then 138 | echo "ERROR: $tool tried to claim function name \"$function_name\" which is already claimed" 139 | exit 1 140 | else 141 | # It's a valid function name, append the tool_reason 142 | # These go into .function.parameters.properties as a tool_reason JSON object, which has type and description 143 | # And also add .function.parameters.required tool_reason 144 | 145 | # Define the tool_reason JSON object 146 | tool_reason='{"tool_reason": {"type": "string", "description": "Reason why this tool must be used. e.g. \"This will help me ensure that the command runs without errors, by allowing me to verify that the system is in order. If I do not check the system I cannot find an alternative if there are errors.\""}}' 147 | 148 | # Add the tool_reason object to the parameters object in the pretty_json JSON 149 | pretty_json=$(echo "$pretty_json" | jq --argjson new_param "$tool_reason" '.function.parameters.properties += $new_param') 150 | 151 | # Add tool_reason to the required array 152 | pretty_json=$(echo "$pretty_json" | jq --arg new_param "tool_reason" '.function.parameters.required += [$new_param]') 153 | 154 | TOOL_MAP["$function_name"]="$tool" 155 | OPENAI_TOOLS+="$pretty_json," 156 | fi 157 | else 158 | echo "Unknown tool type \"$type\"." 159 | fi 160 | fi 161 | fi 162 | fi 163 | done 164 | 165 | # Strip the ending , from OPENAI_TOOLS 166 | OPENAI_TOOLS="${OPENAI_TOOLS%,}" 167 | 168 | # Hide the cursor while we're working 169 | trap 'echo -ne "$SHOW_CURSOR"' EXIT # Make sure the cursor is shown when the script exits 170 | echo -e "$HIDE_CURSOR" 171 | 172 | # Check for configuration file existence 173 | if [ ! -f "$CONFIG_FILE" ]; then 174 | # Initialize configuration file with default values 175 | { 176 | echo "key=" 177 | echo "" 178 | echo "hi_contrast=false" 179 | echo "expose_current_dir=true" 180 | echo "max_history=10" 181 | echo "api=https://api.openai.com/v1/chat/completions" 182 | echo "model=gpt-4o-mini" 183 | echo "json_mode=false" 184 | echo "temp=0.1" 185 | echo "tokens=500" 186 | echo "exec_query=" 187 | echo "question_query=" 188 | echo "error_query=" 189 | } >> "$CONFIG_FILE" 190 | fi 191 | 192 | # Read configuration file 193 | config=$(cat "$CONFIG_FILE") 194 | 195 | # API Key 196 | OPENAI_KEY=$(echo "${config[@]}" | grep -oP '(?<=^key=).+') 197 | if [ -z "$OPENAI_KEY" ]; then 198 | # Prompt user to input OpenAI key if not found 199 | echo "To use Bash AI, please input your OpenAI key into the config file located at $CONFIG_FILE" 200 | echo -ne "$SHOW_CURSOR" 201 | exit 1 202 | fi 203 | 204 | # Extract OpenAI URL from configuration 205 | OPENAI_URL=$(echo "${config[@]}" | grep -oP '(?<=^api=).+') 206 | 207 | # Extract OpenAI model from configuration 208 | OPENAI_MODEL=$(echo "${config[@]}" | grep -oP '(?<=^model=).+') 209 | 210 | # Extract OpenAI temperature from configuration 211 | OPENAI_TEMP=$(echo "${config[@]}" | grep -oP '(?<=^temp=).+') 212 | 213 | # Extract OpenAI system execution query from configuration 214 | OPENAI_EXEC_QUERY=$(echo "${config[@]}" | grep -oP '(?<=^exec_query=).+') 215 | 216 | # Extract OpenAI system question query from configuration 217 | OPENAI_QUESTION_QUERY=$(echo "${config[@]}" | grep -oP '(?<=^question_query=).+') 218 | 219 | # Extract OpenAI system error query from configuration 220 | OPENAI_ERROR_QUERY=$(echo "${config[@]}" | grep -oP '(?<=^error_query=).+') 221 | 222 | # Extract maximum token count from configuration 223 | OPENAI_TOKENS=$(echo "${config[@]}" | grep -oP '(?<=^tokens=).+') 224 | #GLOBAL_QUERY+=" All your messages must be less than \"$OPENAI_TOKENS\" tokens." 225 | 226 | # Test if high contrast mode is set in configuration 227 | HI_CONTRAST=$(echo "${config[@]}" | grep -oP '(?<=^hi_contrast=).+') 228 | if [ "$HI_CONTRAST" = true ]; then 229 | INFO_TEXT_COLOR="$RESET_COLOR" 230 | fi 231 | 232 | # Test if we should expose current dir 233 | EXPOSE_CURRENT_DIR=$(echo "${config[@]}" | grep -oP '(?<=^expose_current_dir=).+') 234 | 235 | # Extract maximum history message count from configuration 236 | MAX_HISTORY_COUNT=$(echo "${config[@]}" | grep -oP '(?<=^max_history=).+') 237 | 238 | # Test if GPT JSON mode is set in configuration 239 | JSON_MODE=$(echo "${config[@]}" | grep -oP '(?<=^json_mode=).+') 240 | if [ "$JSON_MODE" = true ]; then 241 | JSON_MODE="\"response_format\": { \"type\": \"json_object\" }," 242 | else 243 | JSON_MODE="" 244 | fi 245 | 246 | # Set default query if not provided in configuration 247 | if [ -z "$OPENAI_EXEC_QUERY" ]; then 248 | OPENAI_EXEC_QUERY="$DEFAULT_EXEC_QUERY" 249 | fi 250 | if [ -z "$OPENAI_QUESTION_QUERY" ]; then 251 | OPENAI_QUESTION_QUERY="$DEFAULT_QUESTION_QUERY" 252 | fi 253 | if [ -z "$OPENAI_ERROR_QUERY" ]; then 254 | OPENAI_ERROR_QUERY="$DEFAULT_ERROR_QUERY" 255 | fi 256 | 257 | # Helper functions 258 | print_info() { 259 | # Return if there's no text 260 | if [ ${#1} -le 0 ]; then 261 | return 262 | fi 263 | echo -ne "${PRE_TEXT}${INFO_TEXT_COLOR}" 264 | echo -n "$1" 265 | echo -e "${RESET_COLOR}" 266 | echo 267 | } 268 | 269 | print_ok() { 270 | # Return if there's no text 271 | if [ ${#1} -le 0 ]; then 272 | return 273 | fi 274 | echo -e "${OK_TEXT_COLOR}$1${RESET_COLOR}" 275 | echo 276 | } 277 | 278 | print_error() { 279 | # Return if there's no text 280 | if [ ${#1} -le 0 ]; then 281 | return 282 | fi 283 | echo -e "${ERROR_TEXT_COLOR}$1${RESET_COLOR}" 284 | echo 285 | } 286 | 287 | print_cancel() { 288 | # Return if there's no text 289 | if [ ${#1} -le 0 ]; then 290 | return 291 | fi 292 | echo -e "${CANCEL_TEXT_COLOR}$1${RESET_COLOR}" 293 | echo 294 | } 295 | 296 | print_cmd() { 297 | # Return if there's no text 298 | if [ ${#1} -le 0 ]; then 299 | return 300 | fi 301 | echo -ne "${PRE_TEXT}${CMD_BG_COLOR}${CMD_TEXT_COLOR}" 302 | echo -n " $1 " 303 | echo -e "${RESET_COLOR}" 304 | echo 305 | } 306 | 307 | print() { 308 | echo -e "${PRE_TEXT}$1" 309 | } 310 | 311 | json_safe() { 312 | # FIX this is a bad way of doing this, and it misses many unsafe characters 313 | echo "$1" | perl -pe 's/\\/\\\\/g; s/"/\\"/g; s/\033/\\\\033/g; s/\n/ /g; s/\r/\\r/g; s/\t/\\t/g' 314 | } 315 | 316 | run_cmd() { 317 | tmpfile=$(mktemp) 318 | if eval "$1" 2>"$tmpfile"; then 319 | # OK 320 | print_ok "[ok]" 321 | rm "$tmpfile" 322 | return 0 323 | else 324 | # ERROR 325 | output=$(cat "$tmpfile") 326 | LAST_ERROR="${output#*"$0": line *: }" 327 | echo "$LAST_ERROR" 328 | rm "$tmpfile" 329 | 330 | # Ask if we should examine the error 331 | if [ ${#LAST_ERROR} -gt 1 ]; then 332 | print_error "[error]" 333 | echo -n "${PRE_TEXT}examine error? [y/N]: " 334 | echo -ne "$SHOW_CURSOR" 335 | read -n 1 -r -s answer 336 | 337 | # Did the user want to examine the error? 338 | if [ "$answer" == "Y" ] || [ "$answer" == "y" ]; then 339 | echo "yes";echo 340 | USER_QUERY="You executed \"$1\". Which returned error \"$LAST_ERROR\"." 341 | QUERY_TYPE="error" 342 | NEEDS_TO_RUN=true 343 | SKIP_USER_QUERY_RESET=true 344 | else 345 | echo "no";echo 346 | fi 347 | else 348 | print_cancel "[cancel]" 349 | fi 350 | return 1 351 | fi 352 | } 353 | 354 | run_tool() { 355 | TOOL_ID="$1" 356 | TOOL_NAME="$2" 357 | TOOL_ARGS="$3" 358 | TOOL_OUTPUT="" 359 | 360 | # Get the function TOOL_NAME from TOOL_MAP IF IT EXISTS! 361 | if [ -z "${TOOL_MAP[$TOOL_NAME]}" ]; then 362 | TOOL_SCRIPT="" 363 | TOOL_OUTPUT="" 364 | else 365 | TOOL_SCRIPT="${TOOL_MAP[$TOOL_NAME]}" 366 | 367 | TOOL_REASON=$(echo "$TOOL_ARGS" | jq -r '.tool_reason') 368 | TOOL_ARGS_READABLE=$(echo "$TOOL_ARGS" | jq -r 'del(.tool_reason)|to_entries|map("\(.key): \(.value)")|.[]' | paste -sd ',' - | awk '{gsub(/,/, ", "); print}') 369 | print_info "$TOOL_REASON" 370 | print_info "Using tool \"$TOOL_NAME\" $TOOL_ARGS_READABLE" 371 | 372 | echo "$TOOL_NAME" >> /tmp/bai_tool_output.txt 373 | echo "$TOOL_ARGS_READABLE" >> /tmp/bai_tool_output.txt 374 | 375 | # Run the execute function from the TOOL_SCRIPT 376 | TOOL_OUTPUT=$(source "$TOOL_SCRIPT"; execute "$TOOL_ARGS") 377 | echo "$TOOL_OUTPUT" >> /tmp/bai_tool_output.txt 378 | echo "" >> /tmp/bai_tool_output.txt 379 | # Trim the output to 1000 characters 380 | TOOL_OUTPUT=${TOOL_OUTPUT:0:1000} 381 | # Make it JSON safe 382 | TOOL_OUTPUT=$(json_safe "$TOOL_OUTPUT") 383 | fi 384 | 385 | # Apply tool output to message history 386 | HISTORY_MESSAGES+=',{ 387 | "role": "tool", 388 | "content": "'"$TOOL_OUTPUT"'", 389 | "tool_call_id": "'"$TOOL_ID"'" 390 | }' 391 | 392 | # Prepare the next run 393 | NEEDS_TO_RUN=true 394 | SKIP_USER_QUERY=true 395 | SKIP_USER_QUERY_RESET=true 396 | SKIP_SYSTEM_MSG=true 397 | } 398 | 399 | # Make sure all queries are JSON safe 400 | DEFAULT_EXEC_QUERY=$(json_safe "$DEFAULT_EXEC_QUERY") 401 | DEFAULT_QUESTION_QUERY=$(json_safe "$DEFAULT_QUESTION_QUERY") 402 | DEFAULT_ERROR_QUERY=$(json_safe "$DEFAULT_ERROR_QUERY") 403 | GLOBAL_QUERY=$(json_safe "$GLOBAL_QUERY") 404 | DYNAMIC_SYSTEM_QUERY=$(json_safe "$DYNAMIC_SYSTEM_QUERY") 405 | 406 | # User AI query and Interactive Mode 407 | USER_QUERY=$* 408 | 409 | # Are we entering interactive mode? 410 | if [ -z "$USER_QUERY" ]; then 411 | INTERACTIVE_MODE=true 412 | print "🤖 ${TITLE_TEXT_COLOR}Bash AI v${VERSION}${RESET_COLOR}" 413 | # List all tools loaded in TOOL_MAP 414 | if [ ${#TOOL_MAP[@]} -gt 0 ]; then 415 | echo 416 | print "🔧 ${TITLE_TEXT_COLOR}Activated Tools${RESET_COLOR}" 417 | for tool in "${!TOOL_MAP[@]}"; do 418 | print "${TITLE_TEXT_COLOR}$tool${RESET_COLOR} from ${TOOL_MAP[$tool]##*/}" 419 | done 420 | fi 421 | echo 422 | print_info "$INTERACTIVE_INFO" 423 | else 424 | INTERACTIVE_MODE=false 425 | NEEDS_TO_RUN=true 426 | fi 427 | 428 | # We're ready to run 429 | RUN_COUNT=0 430 | 431 | # Run as long as we're oin interactive mode, needs to run, or awaiting tool reponse 432 | while [ "$INTERACTIVE_MODE" = true ] || [ "$NEEDS_TO_RUN" = true ] || [ "$AWAIT_TOOL_REPONSE" = true ]; do 433 | # Ask for user query if we're in Interactive Mode 434 | if [ "$SKIP_USER_QUERY" != true ]; then 435 | while [ -z "$USER_QUERY" ]; do 436 | # No query, prompt user for query 437 | echo -ne "$SHOW_CURSOR" 438 | read -e -r -p "Bash AI> " USER_QUERY 439 | echo -e "$HIDE_CURSOR" 440 | 441 | # Check if user wants to quit 442 | if [ "$USER_QUERY" == "exit" ]; then 443 | echo -ne "$SHOW_CURSOR" 444 | print_info "Bye!" 445 | exit 0 446 | fi 447 | done 448 | 449 | # Make sure the query is JSON safe 450 | USER_QUERY=$(json_safe "$USER_QUERY") 451 | fi 452 | 453 | echo -ne "$HIDE_CURSOR" 454 | 455 | # Pretty up user query 456 | USER_QUERY=$(echo "$USER_QUERY" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') 457 | 458 | # Determine if we should use the question query or the execution query 459 | if [ -z "$QUERY_TYPE" ]; then 460 | if [ ${#USER_QUERY} -gt 0 ]; then 461 | if [[ "$USER_QUERY" == *"?"* ]]; then 462 | QUERY_TYPE="question" 463 | else 464 | QUERY_TYPE="execute" 465 | fi 466 | fi 467 | fi 468 | 469 | # Apply the correct query message history 470 | # The options are "execute", "question" and "error" 471 | if [ "$QUERY_TYPE" == "question" ]; then 472 | # QUESTION 473 | CURRENT_QUERY_TYPE_MSG="${OPENAI_QUESTION_QUERY}" 474 | OPENAI_TEMPLATE_MESSAGES='{ 475 | "role": "system", 476 | "content": "'"${GLOBAL_QUERY}${CURRENT_QUERY_TYPE_MSG}"'" 477 | }, 478 | { 479 | "role": "user", 480 | "content": "how do I list all files?" 481 | }, 482 | { 483 | "role": "assistant", 484 | "content": "{ \"info\": \"Use the \\\"ls\\\" command to with the \\\"-a\\\" flag to list all files, including hidden ones, in the current directory.\" }" 485 | }, 486 | { 487 | "role": "user", 488 | "content": "how do I recursively list all the files?" 489 | }, 490 | { 491 | "role": "assistant", 492 | "content": "{ \"info\": \"Use the \\\"ls\\\" command to with the \\\"-aR\\\" flag to list all files recursively, including hidden ones, in the current directory.\" }" 493 | }, 494 | { 495 | "role": "user", 496 | "content": "how do I print hello world?" 497 | }, 498 | { 499 | "role": "assistant", 500 | "content": "{ \"info\": \"Use the \\\"echo\\\" command to print text, and \\\"echo \\\"hello world\\\"\\\" to print your specified text.\" }" 501 | }, 502 | { 503 | "role": "user", 504 | "content": "how do I autocomplete commands?" 505 | }, 506 | { 507 | "role": "assistant", 508 | "content": "{ \"info\": \"Press the Tab key to autocomplete commands, file names, and directories.\" }" 509 | }' 510 | elif [ "$QUERY_TYPE" == "error" ]; then 511 | # ERROR 512 | CURRENT_QUERY_TYPE_MSG="${OPENAI_ERROR_QUERY}" 513 | OPENAI_TEMPLATE_MESSAGES='{ 514 | "role": "system", 515 | "content": "'"${GLOBAL_QUERY}${CURRENT_QUERY_TYPE_MSG}"'" 516 | }, 517 | { 518 | "role": "user", 519 | "content": "You executed \\\"start avidemux\\\". Which returned error \\\"avidemux: command not found\\\"." 520 | }, 521 | { 522 | "role": "assistant", 523 | "content": "{ \"cmd\": \"sudo install avidemux\", \"info\": \"This means that the application \\\"avidemux\\\" was not found. Try installing it.\" }" 524 | }, 525 | { 526 | "role": "user", 527 | "content": "You executed \\\"cd \\\"hell word\\\"\\\". Which returned error \\\"cd: hell word: No such file or directory\\\"." 528 | }, 529 | { 530 | "role": "assistant", 531 | "content": "{ \"cmd\": \"cd \\\"wORLD helloz\\\"\", \"info\": \"The error indicates that the \\\"wORLD helloz\\\" directory does not exist. However, the current directory contains a \\\"hello world\\\" directory we can try instead.\" }" 532 | }, 533 | { 534 | "role": "user", 535 | "content": "You executed \\\"cat \\\"in .sh.\\\"\\\". Which returned error \\\"cat: in .sh: No such file or directory\\\"." 536 | }, 537 | { 538 | "role": "assistant", 539 | "content": "{ \"cmd\": \"cat \\\"install.sh\\\"\", \"info\": \"The cat command could not find the \\\"in .sh\\\" file in the current directory. However, the current directory contains a file called \\\"install.sh\\\".\" }" 540 | }' 541 | else 542 | # COMMAND 543 | CURRENT_QUERY_TYPE_MSG="${OPENAI_EXEC_QUERY}" 544 | OPENAI_TEMPLATE_MESSAGES='{ 545 | "role": "system", 546 | "content": "'"${GLOBAL_QUERY}${CURRENT_QUERY_TYPE_MSG}"'" 547 | }, 548 | { 549 | "role": "user", 550 | "content": "list all files" 551 | }, 552 | { 553 | "role": "assistant", 554 | "content": "{ \"cmd\": \"ls -a\", \"info\": \"\\\"ls\\\" with the flag \\\"-a\\\" will list all files, including hidden ones, in the current directory\" }" 555 | }, 556 | { 557 | "role": "user", 558 | "content": "start avidemux" 559 | }, 560 | { 561 | "role": "assistant", 562 | "content": "{ \"cmd\": \"avidemux\", \"info\": \"start the Avidemux video editor, if it is installed on the system and available for the current user\" }" 563 | }, 564 | { 565 | "role": "user", 566 | "content": "print hello world" 567 | }, 568 | { 569 | "role": "assistant", 570 | "content": "{ \"cmd\": \"echo \\\"hello world\\\"\", \"info\": \"\\\"echo\\\" will print text, while \\\"echo \\\"hello world\\\"\\\" will print your text\" }" 571 | }, 572 | { 573 | "role": "user", 574 | "content": "remove the hello world folder" 575 | }, 576 | { 577 | "role": "assistant", 578 | "content": "{ \"cmd\": \"rm -r \\\"hello world\\\"\", \"info\": \"\\\"rm\\\" with the \\\"-r\\\" flag will remove the \\\"hello world\\\" folder and its contents recursively\" }" 579 | }, 580 | { 581 | "role": "user", 582 | "content": "move into the hello world folder" 583 | }, 584 | { 585 | "role": "assistant", 586 | "content": "{ \"cmd\": \"cd \\\"hello world\\\"\", \"info\": \"\\\"cd\\\" will let you change directory to \\\"hello world\\\"\" }" 587 | }, 588 | { 589 | "role": "user", 590 | "content": "add /home/user/.local/bin to PATH" 591 | }, 592 | { 593 | "role": "assistant", 594 | "content": "{ \"cmd\": \"export PATH=/home/user/.local/bin:PATH\", \"info\": \"\\\"export\\\" has the ability to add \\\"/some/path\\\" to your PATH environment variable for the current session. the specified path already exists in your PATH environment variable since before\" }" 595 | }' 596 | fi 597 | 598 | # Notify the user about our progress 599 | echo -ne "${PRE_TEXT} $PROGRESS_TEXT" 600 | 601 | # Start the spinner in the background 602 | spinner() { 603 | while :; do 604 | for (( i=0; i<${#PROGRESS_ANIM}; i++ )); do 605 | sleep 0.1 606 | # Print a carriage return (\r) and then the spinner character 607 | echo -ne "\r${PRE_TEXT}${PROGRESS_ANIM:$i:1}" 608 | done 609 | done 610 | } 611 | spinner & # Start the spinner 612 | spinner_pid=$! # Save the spinner's PID 613 | 614 | # If this is the first run we apply history 615 | if [ $RUN_COUNT -eq 0 ]; then 616 | # Check if the history file exists 617 | if [ -f "$HISTORY_FILE" ]; then 618 | # Read the history file 619 | HISTORY_MESSAGES=$(sed 's/^\[\(.*\)\]$/,\1/' $HISTORY_FILE) 620 | fi 621 | fi 622 | 623 | # Prepare system message 624 | if [ "$SKIP_SYSTEM_MSG" != true ]; then 625 | sys_msg="" 626 | # Directory and content exposure 627 | # Check if EXPOSE_CURRENT_DIR is true 628 | if [ "$EXPOSE_CURRENT_DIR" = true ]; then 629 | sys_msg+="User is working from directory \\\"$(json_safe "$(pwd)")\\\"." 630 | fi 631 | # Apply date 632 | sys_msg+=" The current date is Y-m-d H:M \\\"$(date "+%Y-%m-%d %H:%M")\\\"." 633 | # Apply dynamic system query 634 | sys_msg+="$DYNAMIC_SYSTEM_QUERY" 635 | # Apply the system message to history 636 | LAST_HISTORY_MESSAGE=',{ 637 | "role": "system", 638 | "content": "'"${sys_msg}"'" 639 | }' 640 | HISTORY_MESSAGES+="$LAST_HISTORY_MESSAGE" 641 | fi 642 | 643 | # Apply the user to the message history 644 | if [ ${#USER_QUERY} -gt 0 ]; then 645 | HISTORY_MESSAGES+=',{ 646 | "role": "user", 647 | "content": "'${USER_QUERY}'" 648 | }' 649 | fi 650 | 651 | # Construct the JSON payload if we don't already have one 652 | if [ -z "$JSON_PAYLOAD" ]; then 653 | JSON_PAYLOAD='{ 654 | "model": "'"$OPENAI_MODEL"'", 655 | "max_tokens": '"$OPENAI_TOKENS"', 656 | "temperature": '"$OPENAI_TEMP"', 657 | '"$JSON_MODE"' 658 | "messages": ['"$OPENAI_TEMPLATE_MESSAGES $HISTORY_MESSAGES 659 | ,{\"role\": \"system\", \"content\": \"$CURRENT_QUERY_TYPE_MSG Respond in less than $OPENAI_TOKENS tokens.\"} 660 | "']' 661 | 662 | # Apply tools to payload 663 | if [ ${#OPENAI_TOOLS} -gt 0 ]; then 664 | JSON_PAYLOAD+=', "tools": ['"$OPENAI_TOOLS"'], "tool_choice": "auto"' 665 | fi 666 | 667 | # Close the JSON payload 668 | JSON_PAYLOAD+='}' 669 | fi 670 | 671 | # Prettify the JSON payload and verify it 672 | JSON_PAYLOAD=$(echo "$JSON_PAYLOAD" | jq .) 673 | 674 | # Do we have a special URL? 675 | if [ -z "$SPECIAL_API_URL" ]; then 676 | URL="$OPENAI_URL" 677 | else 678 | URL="$SPECIAL_API_URL" 679 | fi 680 | 681 | # Save the payload to a tmp JSON file 682 | echo "$JSON_PAYLOAD" > /tmp/bai_payload.json 683 | 684 | # Send request to OpenAI API 685 | RESPONSE=$(curl -s -X POST -H "Authorization:Bearer $OPENAI_KEY" -H "Content-Type:application/json" -d "$JSON_PAYLOAD" "$URL") 686 | 687 | # Save reponse to a tmp JSON file 688 | echo "$RESPONSE" > /tmp/bai_response.json 689 | 690 | # Stop the spinner 691 | kill $spinner_pid 692 | wait $spinner_pid 2>/dev/null 693 | 694 | # Reset the JSON_PAYLOAD 695 | JSON_PAYLOAD="" 696 | 697 | # Reset the needs to run flag 698 | NEEDS_TO_RUN=false 699 | 700 | # Reset SKIP_USER_QUERY flag 701 | SKIP_USER_QUERY=false 702 | 703 | # Reset SKIP_SYSTEM_MSG flag 704 | SKIP_SYSTEM_MSG=false 705 | 706 | # Reset user query 707 | USER_QUERY="" 708 | 709 | # Is response empty? 710 | if [ -z "$RESPONSE" ]; then 711 | # We didn't get a reply 712 | print_info "$NO_REPLY_TEXT" 713 | echo -ne "$SHOW_CURSOR" 714 | exit 1 715 | fi 716 | 717 | # Extract the reply from the JSON response 718 | REPLY=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // ""') 719 | 720 | # Was there an error? 721 | if [ ${#REPLY} -le 1 ]; then 722 | REPLY=$(echo "$RESPONSE" | jq -r '.error.message // "An unknown error occurred."') 723 | fi 724 | 725 | echo -ne "$CLEAR_LINE\r" 726 | 727 | # Check if there was a reason for stopping 728 | FINISH_REASON=$(echo "$RESPONSE" | jq -r '.choices[0].finish_reason // ""') 729 | 730 | # If the reason IS NOT stop 731 | if [ "$FINISH_REASON" != "stop" ]; then 732 | if [ "$FINISH_REASON" == "length" ]; then 733 | 734 | # Check if the last character is a closing brace 735 | if [[ "${REPLY: -1}" != "}" ]]; then 736 | REPLY+="\"}" 737 | fi 738 | 739 | # Check if the number of opening and closing braces match 740 | while [[ $(tr -cd '{' <<< "$REPLY" | wc -c) -gt $(tr -cd '}' <<< "$REPLY" | wc -c) ]]; do 741 | REPLY+="}" 742 | done 743 | 744 | # Check if the number of double quotes is even 745 | if (( $(tr -cd '"' <<< "$REPLY" | wc -c) % 2 != 0 )); then 746 | REPLY+="\\\"" 747 | fi 748 | 749 | # Replace any unescaped single backslashes with double backslashes 750 | REPLY="${REPLY//\\\\/\\\\\\\\}" 751 | elif [ "$FINISH_REASON" == "content_filter" ]; then 752 | REPLY="Your query was rejected." 753 | elif [ "$FINISH_REASON" == "tool_calls" ]; then 754 | # One or multiple tools were called for 755 | TOOL_CALLS_COUNT=$(echo "$RESPONSE" | jq '.choices[0].message.tool_calls | length') 756 | 757 | for ((i=0; i<$TOOL_CALLS_COUNT; i++)); do 758 | TOOL_ID=$(echo "$RESPONSE" | jq -r '.choices[0].message.tool_calls['"$i"'].id') 759 | TOOL_NAME=$(echo "$RESPONSE" | jq -r '.choices[0].message.tool_calls['"$i"'].function.name') 760 | TOOL_ARGS=$(echo "$RESPONSE" | jq -r '.choices[0].message.tool_calls['"$i"'].function.arguments') 761 | 762 | # Get return from run_tool and apply to our history 763 | HISTORY_MESSAGES+=',{ 764 | "role": "assistant", 765 | "content": null, 766 | "tool_calls": [ 767 | { 768 | "id": "'"$TOOL_ID"'", 769 | "type": "function", 770 | "function": { 771 | "name": "'"$TOOL_NAME"'", 772 | "arguments": "'"$(json_safe "$TOOL_ARGS")"'" 773 | } 774 | } 775 | ] 776 | }' 777 | 778 | run_tool "$TOOL_ID" "$TOOL_NAME" "$TOOL_ARGS" 779 | done 780 | REPLY="" 781 | fi 782 | fi 783 | 784 | # If we still have a reply 785 | if [ ${#REPLY} -gt 1 ]; then 786 | # Try to assemble a JSON object from the REPLY 787 | JSON_CONTENT=$(echo "$REPLY" | perl -0777 -pe 's/.*?(\{.*?\})(\n| ).*/$1/s') 788 | JSON_CONTENT=$(echo "$JSON_CONTENT" | jq -r . 2>/dev/null) 789 | 790 | # Was there JSON content? 791 | if [ ${#JSON_CONTENT} -le 1 ]; then 792 | # No JSON content, use the REPLY as is 793 | JSON_CONTENT="{\"info\": \"$REPLY\"}" 794 | fi 795 | 796 | # Apply the message to history 797 | HISTORY_MESSAGES+=',{ 798 | "role": "assistant", 799 | "content": "'"$(json_safe "$JSON_CONTENT")"'" 800 | }' 801 | 802 | # Extract cmd 803 | CMD=$(echo "$JSON_CONTENT" | jq -r '.cmd // ""' 2>/dev/null) 804 | 805 | # Extract info 806 | INFO=$(echo "$JSON_CONTENT" | jq -r '.info // ""' 2>/dev/null) 807 | 808 | # Check if CMD is empty 809 | if [ ${#CMD} -le 0 ]; then 810 | # Not a command 811 | if [ ${#INFO} -le 0 ]; then 812 | # No info 813 | print_info "$REPLY" 814 | else 815 | # Print info 816 | print_info "$INFO" 817 | fi 818 | echo -ne "$SHOW_CURSOR" 819 | else 820 | # Make sure we have some info 821 | if [ ${#INFO} -le 0 ]; then 822 | INFO="warning: no information" 823 | fi 824 | 825 | # Print command and information 826 | print_cmd "$CMD" 827 | print_info "$INFO" 828 | 829 | # Ask for user command confirmation 830 | echo -n "${PRE_TEXT}execute command? [y/e/N]: " 831 | echo -ne "$SHOW_CURSOR" 832 | read -n 1 -r -s answer 833 | 834 | # Did the user want to edit the command? 835 | if [ "$answer" == "Y" ] || [ "$answer" == "y" ]; then 836 | # RUN 837 | echo "yes";echo 838 | run_cmd "$CMD" 839 | elif [ "$answer" == "E" ] || [ "$answer" == "e" ]; then 840 | # EDIT 841 | echo -ne "$CLEAR_LINE\r" 842 | read -e -r -p "${PRE_TEXT}edit command: " -i "$CMD" CMD 843 | echo 844 | run_cmd "$CMD" 845 | else 846 | # CANCEL 847 | echo "no";echo 848 | print_cancel "[cancel]" 849 | fi 850 | fi 851 | fi 852 | 853 | # Reset user query type unless SKIP_USER_QUERY_RESET is true 854 | if [ "$SKIP_USER_QUERY_RESET" != true ]; then 855 | QUERY_TYPE="" 856 | fi 857 | SKIP_USER_QUERY_RESET=false 858 | 859 | RUN_COUNT=$((RUN_COUNT+1)) 860 | done 861 | 862 | # Save the history messages 863 | if [ "$INTERACTIVE_MODE" = false ]; then 864 | # Add a dummy message at the beginning to make HISTORY_MESSAGES a valid JSON array 865 | HISTORY_MESSAGES_JSON="[null$HISTORY_MESSAGES]" 866 | 867 | # Get the number of messages 868 | HISTORY_COUNT=$(echo "$HISTORY_MESSAGES_JSON" | jq 'length') 869 | 870 | # Convert MAX_HISTORY_COUNT to an integer 871 | MAX_HISTORY_COUNT_INT=$((MAX_HISTORY_COUNT)) 872 | 873 | # If the history is too long, remove the oldest messages 874 | if (( HISTORY_COUNT > MAX_HISTORY_COUNT_INT )); then 875 | HISTORY_MESSAGES_JSON=$(echo "$HISTORY_MESSAGES_JSON" | jq ".[-$MAX_HISTORY_COUNT_INT:]") 876 | fi 877 | 878 | # Remove the dummy message and write the history to the file 879 | echo "$HISTORY_MESSAGES_JSON" | jq '.[1:]' | jq -c . > $HISTORY_FILE 880 | fi 881 | 882 | # We're done 883 | exit 0 884 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- Mode: sh; coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- 3 | 4 | REPO_OWNER="Hezkore" 5 | REPO_NAME="bash-ai" 6 | REPO_BRANCH="main" 7 | REPO_SCRIPT="bai.sh" 8 | SCRIPT_URL="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}/${REPO_SCRIPT}" 9 | BIN_DIR="/usr/local/bin" 10 | BIN_NAME="bai" 11 | TMP_FILE=$(mktemp) 12 | 13 | # Download the script file 14 | echo 15 | echo "Downloading Bash AI..." 16 | curl -q --fail --location --progress-bar --output "$TMP_FILE" "$SCRIPT_URL" 17 | ret=$? 18 | echo 19 | 20 | # Check if curl succeeded 21 | if [ $ret -ne 0 ]; then 22 | echo "Failed to download $REPO_SCRIPT from $SCRIPT_URL" 23 | exit 1 24 | fi 25 | if [ ! -f "$TMP_FILE" ]; then # curl succeeded but file doesn't exist 26 | echo "Failed to create $TMP_FILE" 27 | exit 1 28 | fi 29 | if [ ! -s "$TMP_FILE" ]; then # file exists but is empty 30 | echo "Downloaded $TMP_FILE is empty" 31 | exit 1 32 | fi 33 | 34 | # Move the temp file to bin dir with a proper name 35 | echo "Installing Bash AI to $BIN_DIR..." 36 | sudo mv "$TMP_FILE" "$BIN_DIR/$BIN_NAME" 37 | if [ ! -f "$BIN_DIR/$BIN_NAME" ]; then 38 | echo "Failed to install Bash AI to $BIN_DIR" 39 | exit 1 40 | fi 41 | 42 | # Make the bin executable 43 | sudo chmod +x "$BIN_DIR/$BIN_NAME" 44 | if [ ! -x "$BIN_DIR/$BIN_NAME" ]; then 45 | echo "Failed to make Bash AI executable" 46 | exit 1 47 | fi 48 | 49 | # Done! 50 | echo 51 | echo "Installation completed successfully!" 52 | echo "Run '${BIN_NAME}' to start Bash AI (you may need to restart your terminal)" 53 | echo "Visit https://github.com/${REPO_OWNER}/${REPO_NAME} for more information" 54 | exit 0 -------------------------------------------------------------------------------- /tools/cat.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- Mode: sh; coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- 3 | 4 | # Enables Bash AI to see file content 5 | 6 | init() { 7 | echo '{ 8 | "type": "function", 9 | "function": { 10 | "name": "cat", 11 | "description": "Use this to get the content of any file. Do not use on binary files.", 12 | "parameters": { 13 | "type": "object", 14 | "properties": { 15 | "path": { 16 | "type": "string", 17 | "description": "The absolute path e.g. /home/user/test.txt" 18 | } 19 | }, 20 | "required": [ 21 | "path" 22 | ] 23 | } 24 | } 25 | }' 26 | } 27 | 28 | execute() { 29 | local path 30 | path=$(echo "$1" | jq -r '.path') 31 | output=$(awk '{printf "LINE %d: %s\\\\n", NR, $0}' "$path" 2>&1) 32 | if [ $? -eq 0 ]; then 33 | echo -e "$output" 34 | else 35 | echo "$output" 36 | fi 37 | } -------------------------------------------------------------------------------- /tools/find-wild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- Mode: sh; coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- 3 | 4 | # Enables Bash AI to search for files and directories and forces wildcards 5 | 6 | init() { 7 | echo '{ 8 | "type": "function", 9 | "function": { 10 | "name": "find-wildcard", 11 | "description": "Use this to find any file or directory.", 12 | "parameters": { 13 | "type": "object", 14 | "properties": { 15 | "path": { 16 | "type": "string", 17 | "description": "The path to search recursivly from" 18 | }, 19 | "name": { 20 | "type": "string", 21 | "description": "The iname to search for" 22 | } 23 | }, 24 | "required": [ 25 | "path", 26 | "name" 27 | ] 28 | } 29 | } 30 | }' 31 | } 32 | 33 | execute() { 34 | local path 35 | local name 36 | path=$(echo "$1" | jq -r '.path') 37 | name=$(echo "$1" | jq -r '.name') 38 | name="*$name*" 39 | output=$(eval "find $path -iname '$name'" 2>/dev/null) 40 | if [ -n "$output" ]; then 41 | output=$(echo "$output" | awk '{printf "%s\\n", $0}') 42 | echo "$output" 43 | else 44 | echo "Not found" 45 | fi 46 | } -------------------------------------------------------------------------------- /tools/ls.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- Mode: sh; coding: utf-8; indent-tabs-mode: t; tab-width: 4 -*- 3 | 4 | # Enables Bash AI to list directory content 5 | 6 | init() { 7 | echo '{ 8 | "type": "function", 9 | "function": { 10 | "name": "ls", 11 | "description": "Get content of from any directory. Find correct name for file or directory. Fix directory or file name typos.", 12 | "parameters": { 13 | "type": "object", 14 | "properties": { 15 | "path": { 16 | "type": "string", 17 | "description": "The absolute path e.g. /home/user/Download to list" 18 | } 19 | }, 20 | "required": [ 21 | "path" 22 | ] 23 | } 24 | } 25 | }' 26 | } 27 | 28 | execute() { 29 | local path 30 | path=$(echo "$1" | jq -r '.path') 31 | output=$(ls -1F "$path" 2>&1) 32 | if [ $? -eq 0 ]; then 33 | echo "$output" | awk '{printf "%s\\n", $0}' 34 | else 35 | echo "$output" 36 | fi 37 | } --------------------------------------------------------------------------------