├── .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 | }
--------------------------------------------------------------------------------