├── preview.png ├── .gitattributes ├── .npmignore ├── plugins ├── commit.sh ├── speak.sh ├── translate.sh └── README.md ├── scripts └── postinstall.js ├── package.json ├── LICENSE ├── README.md └── ask.sh /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBXark/ask.sh/HEAD/preview.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development files 2 | .git/ 3 | .gitignore 4 | .DS_Store 5 | *.log 6 | 7 | # Documentation that's not needed in the package 8 | preview.png 9 | 10 | # Keep only essential files for the CLI tool 11 | # ask.sh, plugins/, scripts/, README.md, LICENSE, package.json will be included -------------------------------------------------------------------------------- /plugins/commit.sh: -------------------------------------------------------------------------------- 1 | # Example: Execute the command in the current directory to obtain additional input. 2 | 3 | gen_content() { 4 | local context=$(git diff --cached) 5 | echo "Generate git commit messages based on git diff output according to the standard commit specification. You must return only the commit message without any other text or quotes. Format of the Commit Message: {type}: {subject}. Allowed Types: feat, fix, docs, style, refactor, test, chore\n. Here is the git diff output:\n$context" 6 | } 7 | -------------------------------------------------------------------------------- /plugins/speak.sh: -------------------------------------------------------------------------------- 1 | gen_content() { 2 | local question=$1 3 | local context=$2 4 | echo "$question: $context" 5 | } 6 | 7 | # Example: Customize the processing of AI return results. 8 | after_ask() { 9 | local result=$1 10 | echo $result 11 | # "say" is a tool that comes with Mac, so first check if the "say" command exists. 12 | if ! command -v say &> /dev/null 13 | then 14 | echo "Command 'say' could not be found" 15 | return 16 | fi 17 | say -v Meijia $result 18 | } -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { execSync } = require('child_process'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | console.log('Setting up ask.sh...'); 8 | 9 | // Make ask.sh executable 10 | const askPath = path.join(__dirname, '..', 'ask.sh'); 11 | if (fs.existsSync(askPath)) { 12 | execSync(`chmod +x "${askPath}"`); 13 | console.log('ask.sh has been installed successfully!'); 14 | console.log('You can now use \'ask\' command in your terminal.'); 15 | console.log(''); 16 | console.log('To get started, set up your API key:'); 17 | console.log(' ask set-config api_key YOUR_API_KEY'); 18 | console.log(''); 19 | console.log('For help, run:'); 20 | console.log(' ask --help'); 21 | } else { 22 | console.error('Error: ask.sh not found'); 23 | process.exit(1); 24 | } -------------------------------------------------------------------------------- /plugins/translate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # echo "Location: $(pwd)" 4 | # You can use same context of ask.sh 5 | 6 | # Plugin scripts must include a gen_content function, which takes two parameters. The first parameter is the user's input and the second parameter is the pipeline input. 7 | gen_content() { 8 | local question=$1 # user's input: target language 9 | local context=$2 # pipeline input:need translation text 10 | 11 | # You can only have one /dev/stdin output. If your other commands may also cause output, you need to redirect them elsewhere. 12 | echo "Translate the following text into $question: $context" 13 | } 14 | 15 | 16 | # You can add an "after_ask" function here to customize the processing of AI's return results. You can also choose not to implement it, in which case the default behavior is to output the result. 17 | after_ask() { 18 | local result=$1 # AI's return results 19 | echo $result 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ask.sh", 3 | "version": "0.0.3", 4 | "description": "Ask LLM directly from your terminal", 5 | "keywords": [ 6 | "terminal", 7 | "ai", 8 | "chatgpt", 9 | "cli", 10 | "shell" 11 | ], 12 | "homepage": "https://github.com/TBXark/ask.sh#readme", 13 | "bugs": { 14 | "url": "https://github.com/TBXark/ask.sh/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/TBXark/ask.sh.git" 19 | }, 20 | "license": "MIT", 21 | "author": "TBXark", 22 | "bin": { 23 | "ask": "./ask.sh" 24 | }, 25 | "files": [ 26 | "ask.sh", 27 | "scripts/", 28 | "README.md", 29 | "LICENSE" 30 | ], 31 | "engines": { 32 | "node": ">=12.0.0" 33 | }, 34 | "scripts": { 35 | "test": "bash ask.sh --help", 36 | "postinstall": "node scripts/postinstall.js" 37 | }, 38 | "preferGlobal": true, 39 | "os": [ 40 | "darwin", 41 | "linux" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TBXark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | 4 | 5 | #### 1. [Translate](translate.sh) - Translate text to another language 6 | 7 | This example explains how to use `gen_content` to customize the prompt. 8 | 9 | ```bash 10 | echo "你好,世界" | ask -p translate english 11 | ``` 12 | 13 | 14 | 15 | #### 2. [Commit](commit.sh) - Generate git commit message 16 | 17 | This example explains how you can share the context of the current command in the plugin, which allows you to get more input, such as reading and writing files or executing other commands. 18 | 19 | ```bash 20 | ask -p commit # No need to pass any arguments or pipe 21 | ``` 22 | 23 | As a plugin for [lazygit](https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md) 24 | ```yaml 25 | customCommands: 26 | - key: "" 27 | command: "ask -p commit | tr -d '\n' | tee >(pbcopy)" 28 | context: "files" 29 | loadingText: "Generating commit message..." 30 | description: "Generated commit message by AI" 31 | stream: false 32 | subprocess: false 33 | showOutput: true 34 | outputTitle: "Commit Message (Copied)" 35 | background: true 36 | ``` 37 | 38 | 39 | #### 3. [Speak](speak.sh) - Speak the text with TTS 40 | 41 | This example mainly explains how to use `after_ask` to handle the AI's response. In this example, we used the `say` command to read out the AI's response. 42 | 43 | ```bash 44 | ask -p speak "中国最高的楼是什么" 45 | ``` 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **ask.sh** 2 | 3 | Ask LLM directly from your terminal, and let the AI answer your terminal's output without leaving the terminal. Or generate shell commands you're not familiar with. A bash script will do the trick. You can even manually write plugins to let AI help you do more things. 4 | 5 | ![](./preview.png) 6 | 7 | ## Install 8 | 9 | This script is written in bash, Simply download the script and add execution permissions, this script relies on `curl` and `jq`, make sure they are installed on your system! 10 | 11 | ### Manual Install 12 | ```bash 13 | curl https://raw.githubusercontent.com/TBXark/ask.sh/master/ask.sh > /usr/local/bin/ask 14 | chmod +x /usr/local/bin/ask 15 | ``` 16 | ### Install by npm 17 | ```bash 18 | npm install -g ask.sh 19 | ``` 20 | 21 | ## Supported LLMs 22 | - All OpenAI Compatible LLMs API 23 | 24 | ## Configuration 25 | 26 | ### Config File 27 | ```bash 28 | ask set-config answer_language chinese 29 | ask set-config api_key sk-xxxx 30 | ask set-config api_model deepseek-chat 31 | ask set-config api_endpoint https://api.deepseek.com/chat/completions 32 | ask set-config timeout 60 33 | ask set-config debug false 34 | ``` 35 | 36 | You can also edit `~/.config/ask.sh/config.json` directly 37 | 38 | ### View Configuration 39 | ```bash 40 | ask get-config api_key 41 | ask get-config api_model 42 | # View all available configuration keys 43 | ask get-config 44 | ``` 45 | 46 | ### Environment Variables 47 | If you don't want to use a configuration file, you can set the configuration via environment variables. 48 | ```bash 49 | export ASK_SH_API_KEY=xxx 50 | export ASK_SH_API_MODEL=xxx 51 | export ASK_SH_API_ENDPOINT=xxx 52 | export ASK_SH_ANSWER_LANGUAGE=xxx 53 | export ASK_SH_TIMEOUT=60 54 | export ASK_SH_DEBUG=false 55 | ``` 56 | 57 | Or you can change configuration file path by setting `ASK_SH_CONFIG_FILE` environment variable 58 | 59 | ```bash 60 | export ASK_SH_CONFIG_FILE=/path/to/config.json 61 | export ASK_SH_CONFIG_DIR=/path/to/config/dir 62 | ``` 63 | 64 | 65 | ## Usage 66 | 67 | ### Basic Commands 68 | Generate Shell commands based on questions: 69 | ```bash 70 | ask "What was my last git commit message?" 71 | # Output: 72 | # git log -1 --pretty=%B 73 | ``` 74 | 75 | Using command output as context: 76 | ```bash 77 | ifconfig -a | ask "My local IP" 78 | # Output: 79 | # Your local IP address is `192.168.31.200` 80 | ``` 81 | 82 | ### Debug Mode 83 | Enable debug mode to see detailed request/response information: 84 | ```bash 85 | ask --debug "How to list files?" 86 | # Or set via environment 87 | export ASK_SH_DEBUG=true 88 | ask "How to list files?" 89 | ``` 90 | 91 | ### Configuration Management 92 | ```bash 93 | # Set configuration 94 | ask set-config api_key sk-xxxx 95 | ask set-config timeout 30 96 | 97 | # View configuration 98 | ask get-config api_key 99 | ask get-config timeout 100 | 101 | # View help 102 | ask --help 103 | ask --version 104 | ``` 105 | 106 | ## Plugins 107 | 108 | ### Plugin Management 109 | ```bash 110 | # Install a plugin 111 | ask install-plugin https://raw.githubusercontent.com/TBXark/ask.sh/master/plugins/translate.sh 112 | 113 | # List installed plugins 114 | ask list-plugins 115 | 116 | # Use a plugin 117 | ask -p translate "你好,世界" english 118 | echo "Hello, World" | ask -p translate chinese 119 | ``` 120 | 121 | ### Install Plugin 122 | ```bash 123 | ask install-plugin https://raw.githubusercontent.com/TBXark/ask.sh/master/plugins/translate.sh 124 | ``` 125 | Or you can install the plugin manually in the `~/.config/ask.sh/plugins` directory 126 | 127 | ### Use Plugin 128 | Usage: `ask -p PLUGIN_NAME [ARGS]` or `pipe | ask -p PLUGIN_NAME [ARGS]` 129 | ```bash 130 | echo "你好,世界" | ask -p translate english 131 | # Output: 132 | # Hello, World 133 | ``` 134 | 135 | ### Create Plugin 136 | The plugin is a script file that implements the `gen_content` (required) and `after_ask` (optional) functions. The `gen_content` function is used to generate the context of the question, and the `after_ask` function is used to process AI's response. 137 | 138 | In `after_ask`, you can do many things, such as writing the result to a file or directly executing the command returned by AI. 139 | 140 | For details, please refer to [example](./plugins) 141 | 142 | ## Command Reference 143 | 144 | ### Basic Usage 145 | ```bash 146 | ask "your question" # Ask a question 147 | command | ask "explain this output" # Use command output as context 148 | ask --debug "question" # Enable debug mode 149 | ask --help # Show help 150 | ask --version # Show version 151 | ``` 152 | 153 | ### Configuration Commands 154 | ```bash 155 | ask set-config # Set configuration value 156 | ask get-config # Get configuration value 157 | ``` 158 | 159 | **Available configuration keys:** 160 | - `api_key` - API key for LLM service 161 | - `api_model` - Model name (default: gpt-5-nano) 162 | - `api_endpoint` - API endpoint URL (default: OpenAI API) 163 | - `answer_language` - Response language (default: english) 164 | - `timeout` - Request timeout in seconds (default: 60) 165 | - `debug` - Enable debug mode (default: false) 166 | 167 | ### Plugin Commands 168 | ```bash 169 | ask install-plugin # Install plugin from URL 170 | ask list-plugins # List installed plugins 171 | ask -p [args] # Use a plugin 172 | ``` 173 | 174 | ### Environment Variables 175 | All configuration can be overridden using environment variables: 176 | - `ASK_SH_API_KEY` 177 | - `ASK_SH_API_MODEL` 178 | - `ASK_SH_API_ENDPOINT` 179 | - `ASK_SH_ANSWER_LANGUAGE` 180 | - `ASK_SH_TIMEOUT` 181 | - `ASK_SH_DEBUG` 182 | - `ASK_SH_CONFIG_FILE` - Custom config file path 183 | - `ASK_SH_CONFIG_DIR` - Custom config directory path 184 | 185 | 186 | ## Thanks 187 | This project was inspired by the [egoist/shell-ask](https://github.com/egoist/shell-ask) project, but since it has a dependency on nodejs, I decided to rewrite it in bash 188 | 189 | ## License 190 | **ask.sh** is released under the MIT license. See LICENSE for details. 191 | -------------------------------------------------------------------------------- /ask.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o pipefail 4 | 5 | VERSION="0.0.3" 6 | 7 | api_key=${ASK_SH_API_KEY:-""} 8 | api_model=${ASK_SH_API_MODEL:-"gpt-5-nano"} 9 | api_endpoint=${ASK_SH_API_ENDPOINT:-"https://api.openai.com/v1/chat/completions"} 10 | answer_language=${ASK_SH_ANSWER_LANGUAGE:-"english"} 11 | config_dir=${ASK_SH_CONFIG_DIR:-"$HOME/.config/ask.sh"} 12 | config_file=${ASK_SH_CONFIG_FILE:-"$config_dir/config.json"} 13 | timeout=${ASK_SH_TIMEOUT:-60} 14 | debug=${ASK_SH_DEBUG:-false} 15 | 16 | RED='\033[0;31m' 17 | GREEN='\033[0;32m' 18 | YELLOW='\033[1;33m' 19 | NC='\033[0m' 20 | 21 | log_error() { 22 | echo -e "${RED}Error: $1${NC}" >&2 23 | } 24 | 25 | log_warning() { 26 | echo -e "${YELLOW}Warning: $1${NC}" >&2 27 | } 28 | 29 | log_info() { 30 | echo -e "${GREEN}Info: $1${NC}" >&2 31 | } 32 | 33 | log_debug() { 34 | if [ "$debug" = "true" ]; then 35 | echo -e "Debug: $1" >&2 36 | fi 37 | } 38 | 39 | check_dependencies() { 40 | local missing_deps=() 41 | 42 | if ! command -v curl >/dev/null 2>&1; then 43 | missing_deps+=("curl") 44 | fi 45 | 46 | if ! command -v jq >/dev/null 2>&1; then 47 | missing_deps+=("jq") 48 | fi 49 | 50 | if [ ${#missing_deps[@]} -ne 0 ]; then 51 | log_error "Missing dependencies: ${missing_deps[*]}" 52 | log_info "Please install them using your package manager." 53 | log_info "For example: brew install curl jq (macOS) or apt-get install curl jq (Ubuntu)" 54 | exit 1 55 | fi 56 | } 57 | 58 | validate_config() { 59 | if [ -z "$api_key" ]; then 60 | log_error "API key is not set. Please set it using:" 61 | echo " ask set-config api_key YOUR_API_KEY" 62 | echo " or set ASK_SH_API_KEY environment variable" 63 | exit 1 64 | fi 65 | 66 | if [ -z "$api_endpoint" ]; then 67 | log_error "API endpoint is not set" 68 | exit 1 69 | fi 70 | 71 | if [ -z "$api_model" ]; then 72 | log_error "API model is not set" 73 | exit 1 74 | fi 75 | } 76 | 77 | escape_json() { 78 | local input="$1" 79 | echo "$input" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n$//' 80 | } 81 | 82 | send_request() { 83 | local content="$1" 84 | 85 | log_debug "Sending request to: $api_endpoint" 86 | log_debug "Using model: $api_model" 87 | 88 | local escaped_content=$(escape_json "$content") 89 | 90 | local body=$(jq -n \ 91 | --arg content "$escaped_content" \ 92 | --arg model "$api_model" \ 93 | '{ 94 | model: $model, 95 | messages: [{"role": "user", "content": $content}], 96 | temperature: 0.3 97 | }') 98 | 99 | log_debug "Request body: $body" 100 | 101 | local response 102 | local http_code 103 | 104 | response=$(curl -s -w "\n%{http_code}" \ 105 | --connect-timeout 10 \ 106 | --max-time "$timeout" \ 107 | "$api_endpoint" \ 108 | -H "Content-Type: application/json" \ 109 | -H "Authorization: Bearer $api_key" \ 110 | -d "$body" 2>/dev/null) 111 | 112 | if [ $? -ne 0 ]; then 113 | log_error "Failed to connect to API endpoint. Please check your internet connection and endpoint URL." 114 | exit 1 115 | fi 116 | 117 | http_code=$(echo "$response" | tail -n1) 118 | response_body=$(echo "$response" | sed '$d') 119 | 120 | log_debug "HTTP status code: $http_code" 121 | log_debug "Response body: $response_body" 122 | 123 | if [ "$http_code" -ne 200 ]; then 124 | log_error "API request failed with HTTP status: $http_code" 125 | 126 | local error_message=$(echo "$response_body" | jq -r '.error.message // .message // "Unknown error"' 2>/dev/null) 127 | if [ "$error_message" != "null" ] && [ -n "$error_message" ]; then 128 | log_error "Error details: $error_message" 129 | fi 130 | exit 1 131 | fi 132 | 133 | local error=$(echo "$response_body" | jq -r '.error.message // empty' 2>/dev/null) 134 | 135 | if [ -n "$error" ]; then 136 | log_error "API error: $error" 137 | exit 1 138 | fi 139 | 140 | local result=$(echo "$response_body" | jq -r '.choices[0].message.content // empty' 2>/dev/null) 141 | 142 | if [ -z "$result" ]; then 143 | log_error "Invalid response format from API" 144 | exit 1 145 | fi 146 | 147 | if declare -f after_ask > /dev/null; then 148 | after_ask "$result" 149 | else 150 | echo "$result" 151 | fi 152 | } 153 | 154 | ask() { 155 | local input="" 156 | local prompt="$1" 157 | local content="" 158 | 159 | if [ -p /dev/stdin ]; then 160 | input=$(cat -) 161 | log_debug "Read input from pipe: ${#input} characters" 162 | fi 163 | 164 | if [ -z "$prompt" ]; then 165 | log_error "Please provide a question or prompt" 166 | show_help 167 | exit 1 168 | fi 169 | 170 | if [ -n "$input" ]; then 171 | content="According to the following shell output, using $answer_language to answer the question below: $prompt 172 | 173 | Here is the shell output: 174 | $input" 175 | else 176 | local shell_name=$(basename "$SHELL") 177 | local os_name=$(uname) 178 | content="Return commands suitable for copy/pasting into $shell_name on $os_name. Do NOT include commentary NOR Markdown triple-backtick code blocks as your whole response will be copied into my terminal automatically. 179 | 180 | The script should do this: $prompt" 181 | fi 182 | 183 | log_debug "Generated content length: ${#content} characters" 184 | send_request "$content" 185 | } 186 | 187 | ask_with_plugin() { 188 | local plugin_file="$1" 189 | local prompt="$2" 190 | local input="" 191 | 192 | if [ -p /dev/stdin ]; then 193 | input=$(cat -) 194 | log_debug "Read input from pipe for plugin: ${#input} characters" 195 | fi 196 | 197 | if [ ! -f "$plugin_file" ]; then 198 | log_error "Plugin not found: $plugin_file" 199 | exit 1 200 | fi 201 | 202 | if [ ! -r "$plugin_file" ]; then 203 | log_error "Cannot read plugin file: $plugin_file" 204 | exit 1 205 | fi 206 | 207 | ( 208 | source "$plugin_file" 209 | 210 | if ! declare -f gen_content > /dev/null; then 211 | log_error "Plugin '$plugin_file' must implement 'gen_content' function" 212 | exit 1 213 | fi 214 | 215 | local content 216 | content=$(gen_content "$prompt" "$input") 217 | 218 | if [ $? -ne 0 ] || [ -z "$content" ]; then 219 | log_error "Plugin failed to generate content" 220 | exit 1 221 | fi 222 | 223 | log_debug "Plugin generated content length: ${#content} characters" 224 | send_request "$content" 225 | ) 226 | } 227 | 228 | load_config() { 229 | if [ -f "$config_file" ]; then 230 | log_debug "Loading config from: $config_file" 231 | 232 | if ! jq empty "$config_file" >/dev/null 2>&1; then 233 | log_error "Invalid JSON format in config file: $config_file" 234 | exit 1 235 | fi 236 | 237 | local file_api_key=$(jq -r '.api_key // empty' "$config_file" 2>/dev/null) 238 | local file_api_model=$(jq -r '.api_model // empty' "$config_file" 2>/dev/null) 239 | local file_api_endpoint=$(jq -r '.api_endpoint // empty' "$config_file" 2>/dev/null) 240 | local file_answer_language=$(jq -r '.answer_language // empty' "$config_file" 2>/dev/null) 241 | local file_timeout=$(jq -r '.timeout // empty' "$config_file" 2>/dev/null) 242 | local file_debug=$(jq -r '.debug // empty' "$config_file" 2>/dev/null) 243 | 244 | [ -n "$file_api_key" ] && api_key="$file_api_key" 245 | [ -n "$file_api_model" ] && api_model="$file_api_model" 246 | [ -n "$file_api_endpoint" ] && api_endpoint="$file_api_endpoint" 247 | [ -n "$file_answer_language" ] && answer_language="$file_answer_language" 248 | [ -n "$file_timeout" ] && timeout="$file_timeout" 249 | [ -n "$file_debug" ] && debug="$file_debug" 250 | 251 | log_debug "Configuration loaded successfully" 252 | else 253 | log_debug "No config file found, using environment variables and defaults" 254 | fi 255 | } 256 | 257 | get_config() { 258 | local key="$1" 259 | 260 | if [ -z "$key" ]; then 261 | log_error "Configuration key is required" 262 | echo "Available keys: api_key, api_model, api_endpoint, answer_language, timeout, debug" 263 | exit 1 264 | fi 265 | 266 | if [ -f "$config_file" ]; then 267 | if ! jq empty "$config_file" >/dev/null 2>&1; then 268 | log_error "Invalid JSON format in config file: $config_file" 269 | exit 1 270 | fi 271 | 272 | local value=$(jq -r --arg key "$key" '.[$key] // empty' "$config_file" 2>/dev/null) 273 | if [ -n "$value" ]; then 274 | echo "$value" 275 | return 276 | fi 277 | fi 278 | 279 | case "$key" in 280 | api_key) 281 | echo "$api_key" 282 | ;; 283 | api_model) 284 | echo "$api_model" 285 | ;; 286 | api_endpoint) 287 | echo "$api_endpoint" 288 | ;; 289 | answer_language) 290 | echo "$answer_language" 291 | ;; 292 | timeout) 293 | echo "$timeout" 294 | ;; 295 | debug) 296 | echo "$debug" 297 | ;; 298 | *) 299 | log_error "Unknown configuration key: $key" 300 | exit 1 301 | ;; 302 | esac 303 | } 304 | 305 | set_config() { 306 | local key="$1" 307 | local value="$2" 308 | 309 | if [ -z "$key" ] || [ -z "$value" ]; then 310 | log_error "Both key and value are required" 311 | echo "Usage: ask set-config " 312 | echo "Available keys: api_key, api_model, api_endpoint, answer_language, timeout, debug" 313 | exit 1 314 | fi 315 | 316 | case "$key" in 317 | api_key|api_model|api_endpoint|answer_language|timeout|debug) 318 | ;; 319 | *) 320 | log_error "Invalid configuration key: $key" 321 | echo "Available keys: api_key, api_model, api_endpoint, answer_language, timeout, debug" 322 | exit 1 323 | ;; 324 | esac 325 | 326 | if [ ! -d "$config_dir" ]; then 327 | if ! mkdir -p "$config_dir"; then 328 | log_error "Failed to create config directory: $config_dir" 329 | exit 1 330 | fi 331 | log_debug "Created config directory: $config_dir" 332 | fi 333 | 334 | if [ ! -f "$config_file" ]; then 335 | local initial_config=$(jq -n \ 336 | --arg api_key "$api_key" \ 337 | --arg api_model "$api_model" \ 338 | --arg api_endpoint "$api_endpoint" \ 339 | --arg answer_language "$answer_language" \ 340 | --argjson timeout "$timeout" \ 341 | --arg debug "$debug" \ 342 | '{ 343 | api_key: $api_key, 344 | api_model: $api_model, 345 | api_endpoint: $api_endpoint, 346 | answer_language: $answer_language, 347 | timeout: $timeout, 348 | debug: $debug 349 | }') 350 | 351 | if ! echo "$initial_config" > "$config_file"; then 352 | log_error "Failed to create config file: $config_file" 353 | exit 1 354 | fi 355 | log_debug "Created config file: $config_file" 356 | fi 357 | 358 | if ! jq empty "$config_file" >/dev/null 2>&1; then 359 | log_error "Invalid JSON format in config file: $config_file" 360 | exit 1 361 | fi 362 | 363 | local temp_file="${config_file}.tmp.$$" 364 | if jq --arg key "$key" --arg value "$value" '.[$key] = $value' "$config_file" > "$temp_file"; then 365 | if mv "$temp_file" "$config_file"; then 366 | log_info "Configuration updated: $key = $value" 367 | else 368 | log_error "Failed to update config file" 369 | rm -f "$temp_file" 370 | exit 1 371 | fi 372 | else 373 | log_error "Failed to update configuration" 374 | rm -f "$temp_file" 375 | exit 1 376 | fi 377 | } 378 | 379 | install_plugin() { 380 | local url="$1" 381 | 382 | if [ -z "$url" ]; then 383 | log_error "Plugin URL is required" 384 | echo "Usage: ask install-plugin " 385 | exit 1 386 | fi 387 | 388 | local plugin_name=$(basename "$url") 389 | local plugin_dir="$config_dir/plugins" 390 | local plugin_path="$plugin_dir/$plugin_name" 391 | 392 | if [ ! -d "$plugin_dir" ]; then 393 | if ! mkdir -p "$plugin_dir"; then 394 | log_error "Failed to create plugins directory: $plugin_dir" 395 | exit 1 396 | fi 397 | log_debug "Created plugins directory: $plugin_dir" 398 | fi 399 | 400 | log_info "Downloading plugin from: $url" 401 | if curl -s -f --connect-timeout 10 --max-time 30 "$url" > "$plugin_path"; then 402 | chmod +x "$plugin_path" 403 | log_info "Plugin installed successfully: $plugin_name" 404 | log_info "Usage: ask -p ${plugin_name%.*} [args]" 405 | else 406 | log_error "Failed to download plugin from: $url" 407 | rm -f "$plugin_path" 408 | exit 1 409 | fi 410 | } 411 | 412 | list_plugins() { 413 | local plugin_dir="$config_dir/plugins" 414 | 415 | if [ ! -d "$plugin_dir" ]; then 416 | echo "No plugins directory found" 417 | return 418 | fi 419 | 420 | local plugins=$(find "$plugin_dir" -name "*.sh" -type f 2>/dev/null) 421 | 422 | if [ -z "$plugins" ]; then 423 | echo "No plugins installed" 424 | return 425 | fi 426 | 427 | echo "Installed plugins:" 428 | while IFS= read -r plugin; do 429 | local name=$(basename "$plugin" .sh) 430 | echo " $name" 431 | done <<< "$plugins" 432 | } 433 | 434 | show_help() { 435 | cat << EOF 436 | ask.sh v$VERSION - Ask LLM directly from your terminal 437 | 438 | USAGE: 439 | ask [OPTIONS] "your question" 440 | command | ask "explain this output" 441 | 442 | OPTIONS: 443 | -p, --plugin Use a plugin 444 | -h, --help Show this help message 445 | -v, --version Show version 446 | --debug Enable debug mode 447 | 448 | CONFIGURATION: 449 | ask set-config Set configuration 450 | ask get-config Get configuration 451 | ask list-plugins List installed plugins 452 | ask install-plugin Install plugin from URL 453 | 454 | CONFIGURATION KEYS: 455 | api_key API key for LLM service 456 | api_model Model name (e.g., gpt-5-nano) 457 | api_endpoint API endpoint URL 458 | answer_language Language for responses (e.g., english, chinese) 459 | timeout Request timeout in seconds (default: 30) 460 | debug Enable debug mode (true/false) 461 | 462 | ENVIRONMENT VARIABLES: 463 | ASK_SH_API_KEY Override api_key 464 | ASK_SH_API_MODEL Override api_model 465 | ASK_SH_API_ENDPOINT Override api_endpoint 466 | ASK_SH_ANSWER_LANGUAGE Override answer_language 467 | ASK_SH_TIMEOUT Override timeout 468 | ASK_SH_DEBUG Override debug mode 469 | ASK_SH_CONFIG_FILE Override config file path 470 | 471 | EXAMPLES: 472 | ask "How to find files larger than 1GB?" 473 | ls -la | ask "What's taking up the most space?" 474 | ask -p translate english "Hello world" 475 | ask set-config api_key sk-xxx 476 | ask install-plugin https://raw.githubusercontent.com/TBXark/ask.sh/master/plugins/translate.sh 477 | 478 | For more information, visit: https://github.com/TBXark/ask.sh 479 | EOF 480 | } 481 | 482 | main() { 483 | check_dependencies 484 | 485 | case "$1" in 486 | -h|--help|help) 487 | show_help 488 | exit 0 489 | ;; 490 | -v|--version|version) 491 | echo "ask.sh v$VERSION" 492 | exit 0 493 | ;; 494 | --debug) 495 | debug=true 496 | shift 497 | ;; 498 | set-config) 499 | set_config "$2" "$3" 500 | exit 0 501 | ;; 502 | get-config) 503 | get_config "$2" 504 | exit 0 505 | ;; 506 | list-plugins) 507 | list_plugins 508 | exit 0 509 | ;; 510 | install-plugin) 511 | install_plugin "$2" 512 | exit 0 513 | ;; 514 | -p|--plugin) 515 | if [ -z "$2" ]; then 516 | log_error "Plugin name is required" 517 | show_help 518 | exit 1 519 | fi 520 | 521 | load_config 522 | validate_config 523 | 524 | local plugin_file="$config_dir/plugins/$2.sh" 525 | local prompt="${*:3}" 526 | 527 | ask_with_plugin "$plugin_file" "$prompt" 528 | exit 0 529 | ;; 530 | "") 531 | log_error "Please provide a question or command" 532 | show_help 533 | exit 1 534 | ;; 535 | *) 536 | local args=() 537 | while [ $# -gt 0 ]; do 538 | case "$1" in 539 | --debug) 540 | debug=true 541 | ;; 542 | *) 543 | args+=("$1") 544 | ;; 545 | esac 546 | shift 547 | done 548 | 549 | load_config 550 | validate_config 551 | 552 | ask "${args[*]}" 553 | exit 0 554 | ;; 555 | esac 556 | } 557 | 558 | main "$@" 559 | --------------------------------------------------------------------------------