diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000..898de41 --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,246 @@ +# Creating Plugins for zsh-copilot + +zsh-copilot supports three types of plugins: + +| Type | Purpose | Function Pattern | +|------|---------|------------------| +| **Provider** | AI backend (API calls) | `_zsh_copilot_provider_` | +| **Context** | Add info to prompts | `_zsh_copilot_context_` | +| **Transform** | Process AI responses | `_zsh_copilot_transform_` | + +## Provider Plugins + +Provider plugins handle communication with AI backends. + +### Interface + +```zsh +_zsh_copilot_provider_() { + local input="$1" # User's input (escaped) + local system_prompt="$2" # System prompt + context + + # Make API call, parse response + # Write result to /tmp/zsh_copilot_suggestion + # Write errors to /tmp/.zsh_copilot_error + # Return 0 on success, 1 on failure +} +``` + +### Example: Ollama Provider + +```zsh +# ~/.config/zsh-copilot/plugins/providers/ollama.zsh + +_zsh_copilot_provider_ollama() { + local input="$1" + local system_prompt="$2" + local model=${OLLAMA_MODEL:-"llama2"} + local url=${OLLAMA_URL:-"http://localhost:11434"} + + local data="{ + \"model\": \"$model\", + \"prompt\": \"$system_prompt\n\nUser: $input\", + \"stream\": false + }" + + local response + response=$(curl "$url/api/generate" \ + --silent \ + -H "Content-Type: application/json" \ + -d "$data") + local response_code=$? + + if [[ $response_code -ne 0 ]]; then + echo "Error connecting to Ollama at $url" > /tmp/.zsh_copilot_error + return 1 + fi + + local message + message=$(echo "$response" | jq -r '.response') + + if [[ -z "$message" || "$message" == "null" ]]; then + echo "Empty response from Ollama" > /tmp/.zsh_copilot_error + return 1 + fi + + echo "$message" > /tmp/zsh_copilot_suggestion +} +``` + +### Usage + +```zsh +# In .zshrc +source ~/.config/zsh-copilot/plugins/providers/ollama.zsh +export ZSH_COPILOT_AI_PROVIDER="ollama" +``` + +--- + +## Context Plugins + +Context plugins add information to the prompt sent to the AI. They chain together - each plugin receives the output of previous plugins. + +### Interface + +```zsh +_zsh_copilot_context_() { + local context="$1" # Current context from previous plugins + + local my_info="..." + + # Append to existing context + if [[ -n "$context" ]]; then + echo "$context $my_info" + else + echo "$my_info" + fi +} +``` + +### Example: Git Context + +```zsh +# ~/.config/zsh-copilot/plugins/context/git.zsh + +_zsh_copilot_context_git() { + local context="$1" + + local git_info="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local branch=$(git branch --show-current 2>/dev/null) + local status=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + git_info="Git repo on branch '$branch' with $status uncommitted changes." + fi + + if [[ -n "$context" ]]; then + echo "$context $git_info" + else + echo "$git_info" + fi +} +``` + +### Example: Node.js Project Context + +```zsh +# ~/.config/zsh-copilot/plugins/context/nodejs.zsh + +_zsh_copilot_context_nodejs() { + local context="$1" + + local node_info="" + if [[ -f "package.json" ]]; then + local name=$(jq -r '.name // empty' package.json 2>/dev/null) + local pm="npm" + [[ -f "yarn.lock" ]] && pm="yarn" + [[ -f "pnpm-lock.yaml" ]] && pm="pnpm" + [[ -f "bun.lockb" ]] && pm="bun" + node_info="Node.js project${name:+ '$name'} using $pm." + fi + + if [[ -n "$context" ]]; then + echo "$context $node_info" + else + echo "$node_info" + fi +} +``` + +--- + +## Transform Plugins + +Transform plugins process the AI response before displaying it. They chain together. + +### Interface + +```zsh +_zsh_copilot_transform_() { + local message="$1" # Current message (possibly from previous transforms) + + # Process message + echo "$processed_message" +} +``` + +### Example: Strip Explanations + +```zsh +# ~/.config/zsh-copilot/plugins/transform/strip-explanation.zsh + +_zsh_copilot_transform_strip_explanation() { + local message="$1" + + # Remove common AI prefixes + message="${message#Here is the command: }" + message="${message#The command is: }" + message="${message#Try this: }" + message="${message#Run: }" + + echo "$message" +} +``` + +### Example: Dangerous Command Warning + +```zsh +# ~/.config/zsh-copilot/plugins/transform/warn-dangerous.zsh + +_zsh_copilot_transform_warn_dangerous() { + local message="$1" + + # Check for dangerous patterns + if [[ "$message" == *"rm -rf"* ]] || \ + [[ "$message" == *"sudo rm"* ]] || \ + [[ "$message" == *"> /dev/"* ]]; then + echo "# WARNING: Potentially dangerous command\n$message" + else + echo "$message" + fi +} +``` + +--- + +## Plugin Loading + +Plugins are loaded by sourcing them in your `.zshrc`: + +```zsh +# Source zsh-copilot first +source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh + +# Then source plugins +source ~/.config/zsh-copilot/plugins/providers/openai.zsh +source ~/.config/zsh-copilot/plugins/context/system.zsh +source ~/.config/zsh-copilot/plugins/context/git.zsh +source ~/.config/zsh-copilot/plugins/transform/normalize.zsh +``` + +### Custom Plugin Directory + +You can keep your custom plugins anywhere. A common pattern: + +```zsh +# Custom plugins in ~/.zsh-copilot/plugins/ +for plugin in ~/.zsh-copilot/plugins/**/*.zsh(N); do + source "$plugin" +done +``` + +--- + +## Tips + +1. **Keep plugins fast** - They run on every suggestion request +2. **Handle errors gracefully** - Don't break the shell if something fails +3. **Use `(N)` glob qualifier** - Prevents errors when no files match +4. **Test incrementally** - Source plugins manually before adding to `.zshrc` +5. **Check `$ZSH_COPILOT_DEBUG`** - Log debug info when enabled + +```zsh +if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then + echo "{\"plugin\":\"my-plugin\",\"data\":\"...\"}" >> /tmp/zsh-copilot.log +fi +``` diff --git a/README.md b/README.md index ee40078..9f96827 100644 --- a/README.md +++ b/README.md @@ -10,69 +10,90 @@ https://github.com/Myzel394/zsh-copilot/assets/50424412/ed2bc8ac-ce49-4012-ab73- ### Dependencies -Please make sure you have the following dependencies installed: - * [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions) * [jq](https://github.com/jqlang/jq) * [curl](https://github.com/curl/curl) -### Oh My Zsh +### Recommended Installation -1. Clone `zsh-copilot` into `$ZSH_CUSTOM/plugins` (by default ~/.config/oh-my-zsh/custom/plugins) +It's highly recommended to git clone zsh-copilot into `~/.config/zsh-copilot`. zsh-copilot uses a plugin system and this makes it easy to load the built-in plugins. ```sh -git clone https://git.myzel394.app/Myzel394/zsh-copilot ${ZSH_CUSTOM:-~/.config/oh-my-zsh/custom}/plugins/zsh-copilot +git clone https://git.myzel394.app/Myzel394/zsh-copilot ~/.config/zsh-copilot ``` -2. Add `zsh-copilot` to the plugins array in your `.zshrc` file: - -```bash -plugins=( - # your other plugins... - zsh-autosuggestions -) +Add to your `.zshrc`: +```sh +source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh ``` -### Manual Installation +### Oh My Zsh ```sh -git clone https://git.myzel394.app/Myzel394/zsh-copilot ~/.config/zsh-copilot -echo "source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh" >> ~/.zshrc +git clone https://git.myzel394.app/Myzel394/zsh-copilot ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-copilot ``` +Add `zsh-copilot` to the plugins array in your `.zshrc`. + ## Configuration -You need to have an API key for either OpenAI or Anthropic to use this plugin. Expose this via the appropriate environment variable: +### Loading a Provider + +You need to source a provider plugin. Add one of these to your `.zshrc` after sourcing zsh-copilot: -For OpenAI (default): +**OpenAI:** ```sh -export OPENAI_API_KEY= +export OPENAI_API_KEY= +source ~/.config/zsh-copilot/plugins/providers/openai.zsh ``` -For Anthropic: +**Anthropic:** ```sh -export ANTHROPIC_API_KEY= +export ANTHROPIC_API_KEY= +source ~/.config/zsh-copilot/plugins/providers/anthropic.zsh ``` -You can configure the AI provider using the `ZSH_COPILOT_AI_PROVIDER` variable: +### Loading Plugins + +Source the plugins you want: ```sh -export ZSH_COPILOT_AI_PROVIDER="openai" # or "anthropic" +# Provider (required - pick one) +source ~/.config/zsh-copilot/plugins/providers/openai.zsh + +# Context plugins (optional - adds info to prompts) +source ~/.config/zsh-copilot/plugins/context/system.zsh + +# Transform plugins (optional - processes AI responses) +source ~/.config/zsh-copilot/plugins/transform/normalize.zsh ``` -Other configuration options: +### Built-in Plugins -- `ZSH_COPILOT_KEY`: Key to press to get suggestions (default: ^z) -- `ZSH_COPILOT_SEND_CONTEXT`: If `true`, zsh-copilot will send context information to the AI model (default: true) -- `ZSH_COPILOT_DEBUG`: Enable debug logging (default: false) +Take a look at the [plugins directory](./plugins) for built-in plugins. -To see all available configurations and their current values, run: +| Type | Plugin | Description | +|------|--------|-------------| +| Provider | `openai` | OpenAI API (gpt-4o-mini) | +| Provider | `anthropic` | Anthropic API (claude-3-haiku) | +| Context | `system` | Adds user, shell, terminal, OS info | +| Context | `git` | Adds git repo, branch, and uncommitted changes info | +| Transform | `normalize` | Trims whitespace, removes quotes/code blocks | -```sh -zsh-copilot -``` +### Options + +| Variable | Default | Description | +|----------|---------|-------------| +| `ZSH_COPILOT_KEY` | `^z` | Key binding for suggestions | +| `ZSH_COPILOT_SEND_CONTEXT` | `true` | Send context info to AI | +| `ZSH_COPILOT_DEBUG` | `false` | Enable debug logging | + +Run `zsh-copilot` to see current configuration. ## Usage -Type in your command or your message and press `CTRL + Z` to get your suggestion! +Type your command or message and press `CTRL + Z` to get a suggestion. + +## Creating Custom Plugins +See [PLUGINS.md](PLUGINS.md) for documentation on creating your own providers, context plugins, and transform plugins. diff --git a/_debug.zsh b/_debug.zsh new file mode 100644 index 0000000..6ff8e8b --- /dev/null +++ b/_debug.zsh @@ -0,0 +1,5 @@ +#!/usr/bin/env zsh + +ZSH_COPILOT_DEBUG=true + +source ./zsh-copilot.plugin.zsh && zsh-copilot diff --git a/plugins/context/git.zsh b/plugins/context/git.zsh new file mode 100644 index 0000000..d28f7ef --- /dev/null +++ b/plugins/context/git.zsh @@ -0,0 +1,26 @@ +#!/usr/bin/env zsh +# Git context plugin for zsh-copilot +# +# Adds git repository information to the context. + +_zsh_copilot_context_git() { + local context="$1" + + local git_info="" + if git rev-parse --is-inside-work-tree &>/dev/null; then + local branch=$(git branch --show-current 2>/dev/null) + local repo=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") + local dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + + git_info="You are in git repository '$repo' on branch '$branch'." + if [[ "$dirty" -gt 0 ]]; then + git_info+=" There are $dirty uncommitted changes." + fi + fi + + if [[ -n "$context" ]]; then + echo "$context $git_info" + else + echo "$git_info" + fi +} diff --git a/plugins/context/system.zsh b/plugins/context/system.zsh new file mode 100644 index 0000000..ba6210a --- /dev/null +++ b/plugins/context/system.zsh @@ -0,0 +1,25 @@ +#!/usr/bin/env zsh +# System context plugin for zsh-copilot +# +# Adds user, shell, terminal, and OS information to the context. + +_zsh_copilot_context_system() { + local context="$1" + + local system_info + if [[ "$OSTYPE" == "darwin"* ]]; then + system_info="Your system is ${$(sw_vers | xargs | sed 's/ /./g')}." + else + system_info="Your system is ${$(cat /etc/*-release | xargs | sed 's/ /,/g')}." + fi + + local info="You are user $(whoami) with id $(id) in directory $(pwd). " + info+="Your shell is $SHELL and your terminal is $TERM running on $(uname -a). " + info+="$system_info" + + if [[ -n "$context" ]]; then + echo "$context $info" + else + echo "$info" + fi +} diff --git a/plugins/providers/anthropic.zsh b/plugins/providers/anthropic.zsh new file mode 100644 index 0000000..51d660c --- /dev/null +++ b/plugins/providers/anthropic.zsh @@ -0,0 +1,53 @@ +#!/usr/bin/env zsh +# Anthropic provider for zsh-copilot +# +# Required environment variables: +# ANTHROPIC_API_KEY - Your Anthropic API key +# +# Optional environment variables: +# ANTHROPIC_API_URL - Custom API URL (default: api.anthropic.com) + +_zsh_copilot_provider_anthropic() { + local input="$1" + local system_prompt="$2" + local anthropic_api_url=${ANTHROPIC_API_URL:-"api.anthropic.com"} + + local data="{ + \"model\": \"claude-3-haiku-20240307\", + \"max_tokens\": 500, + \"system\": \"$system_prompt\", + \"messages\": [ + { + \"role\": \"user\", + \"content\": \"$input\" + } + ] + }" + + local response + response=$(curl "https://${anthropic_api_url}/v1/messages" \ + --silent \ + -H "Content-Type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d "$data") + local response_code=$? + + if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then + echo "{\"date\":\"$(date)\",\"log\":\"Called Anthropic API\",\"input\":\"$input\",\"response\":\"$response\",\"response_code\":\"$response_code\"}" >> /tmp/zsh-copilot.log + fi + + if [[ $response_code -ne 0 ]] || [[ $(echo "$response" | jq -r '.type') == 'error' ]]; then + if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then + echo "{\"date\":\"$(date)\",\"log\":\"Error fetching Anthropic\"}" >> /tmp/zsh-copilot.log + fi + + echo "Error fetching suggestions from the Anthropic API. Please check your API key and try again." > /tmp/.zsh_copilot_error + return 1 + fi + + local message + message=$(echo "$response" | tr -d '\n' | jq -r '.content[0].text') + + echo "$message" > /tmp/zsh_copilot_suggestion || return 1 +} diff --git a/plugins/providers/openai.zsh b/plugins/providers/openai.zsh new file mode 100644 index 0000000..f971d17 --- /dev/null +++ b/plugins/providers/openai.zsh @@ -0,0 +1,50 @@ +#!/usr/bin/env zsh +# OpenAI provider for zsh-copilot +# +# Required environment variables: +# OPENAI_API_KEY - Your OpenAI API key +# +# Optional environment variables: +# OPENAI_API_URL - Custom API URL (default: api.openai.com) + +_zsh_copilot_provider_openai() { + local input="$1" + local system_prompt="$2" + local openai_api_url=${OPENAI_API_URL:-"api.openai.com"} + + local data="{ + \"model\": \"gpt-4o-mini\", + \"messages\": [ + { + \"role\": \"system\", + \"content\": \"$system_prompt\" + }, + { + \"role\": \"user\", + \"content\": \"$input\" + } + ] + }" + + local response + response=$(curl "https://${openai_api_url}/v1/chat/completions" \ + --silent \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d "$data") + local response_code=$? + + if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then + echo "{\"date\":\"$(date)\",\"log\":\"Called OpenAI API\",\"input\":\"$input\",\"response\":\"$response\",\"response_code\":\"$response_code\"}" >> /tmp/zsh-copilot.log + fi + + if [[ $response_code -ne 0 ]]; then + echo "Error fetching suggestions from the OpenAI API. Please check your API key and try again." > /tmp/.zsh_copilot_error + return 1 + fi + + local message + message=$(echo "$response" | tr -d '\n' | jq -r '.choices[0].message.content') + + echo "$message" > /tmp/zsh_copilot_suggestion || return 1 +} diff --git a/plugins/transform/normalize.zsh b/plugins/transform/normalize.zsh new file mode 100644 index 0000000..13b3af9 --- /dev/null +++ b/plugins/transform/normalize.zsh @@ -0,0 +1,46 @@ +#!/usr/bin/env zsh +# Normalize transform plugin for zsh-copilot +# +# Cleans up AI responses by: +# - Trimming whitespace +# - Removing surrounding quotes +# - Removing markdown code block markers +# - Stripping common AI prefixes + +_zsh_copilot_transform_normalize() { + local message="$1" + + # Trim leading/trailing whitespace + message="${message#"${message%%[![:space:]]*}"}" + message="${message%"${message##*[![:space:]]}"}" + + # Remove markdown code block markers (```bash, ```sh, ```, etc.) + message="${message#\`\`\`*$'\n'}" + message="${message%$'\n'\`\`\`}" + message="${message#\`\`\`}" + message="${message%\`\`\`}" + + # Remove single backticks wrapping the whole command + if [[ "$message" == \`*\` ]]; then + message="${message#\`}" + message="${message%\`}" + fi + + # Remove surrounding double quotes + if [[ "$message" == \"*\" ]]; then + message="${message#\"}" + message="${message%\"}" + fi + + # Remove surrounding single quotes + if [[ "$message" == \'*\' ]]; then + message="${message#\'}" + message="${message%\'}" + fi + + # Trim again after removals + message="${message#"${message%%[![:space:]]*}"}" + message="${message%"${message##*[![:space:]]}"}" + + echo "$message" +} diff --git a/zsh-copilot.plugin.zsh b/zsh-copilot.plugin.zsh index 867f60a..adc7fd0 100644 --- a/zsh-copilot.plugin.zsh +++ b/zsh-copilot.plugin.zsh @@ -11,15 +11,12 @@ (( ! ${+ZSH_COPILOT_DEBUG} )) && typeset -g ZSH_COPILOT_DEBUG=false -# New option to select AI provider +# AI provider selection (auto-detect if not set) if [[ -z "$ZSH_COPILOT_AI_PROVIDER" ]]; then if [[ -n "$OPENAI_API_KEY" ]]; then typeset -g ZSH_COPILOT_AI_PROVIDER="openai" elif [[ -n "$ANTHROPIC_API_KEY" ]]; then typeset -g ZSH_COPILOT_AI_PROVIDER="anthropic" - else - echo "No AI provider selected. Please set either OPENAI_API_KEY or ANTHROPIC_API_KEY." - return 1 fi fi @@ -27,15 +24,8 @@ fi if [[ -z "$ZSH_COPILOT_SYSTEM_PROMPT" ]]; then read -r -d '' ZSH_COPILOT_SYSTEM_PROMPT <<- EOM You will be given the raw input of a shell command. - Your task is to either complete the command or provide a new command that you think the user is trying to type. - If you return a completely new command for the user, prefix is with an equal sign (=). - If you return a completion for the user's command, prefix it with a plus sign (+). - MAKE SURE TO ONLY INCLUDE THE REST OF THE COMPLETION!!! - Do not write any leading or trailing characters except if required for the completion to work. - - Only respond with either a completion or a new command, not both. - Your response may only start with either a plus sign or an equal sign. - Your response MAY NOT start with both! This means that your response IS NOT ALLOWED to start with '+=' or '=+'. + Your task is to provide a command the user is trying to execute. + Return the whole command that you think the user wants to run. Your response MAY NOT contain any newlines! Do NOT add any additional text, comments, or explanations to your response. @@ -44,11 +34,11 @@ read -r -d '' ZSH_COPILOT_SYSTEM_PROMPT <<- EOM Your response will be run in the user's shell. Make sure input is escaped correctly if needed so. Your input should be able to run without any modifications to it. - DO NOT INTERACT WITH THE USER IN NATURAL LANGUAGE! If you do, you will be banned from the system. + DO NOT INTERACT WITH THE USER IN NATURAL LANGUAGE! Note that the double quote sign is escaped. Keep this in mind when you create quotes. Here are two examples: - * User input: 'list files in current directory'; Your response: '=ls' (ls is the builtin command for listing files) - * User input: 'cd /tm'; Your response: '+p' (/tmp is the standard temp folder on linux and mac). + * User input: 'list files in current directory'; Your response: 'ls' (ls is the builtin command for listing files) + * User input: 'cd /tm'; Your response: 'cd /tmp' (/tmp is the standard temp folder on linux and mac). EOM fi @@ -57,83 +47,16 @@ if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then fi function _fetch_suggestions() { - local data - local response - local message - - if [[ "$ZSH_COPILOT_AI_PROVIDER" == "openai" ]]; then - # OpenAI's API payload - data="{ - \"model\": \"gpt-4o-mini\", - \"messages\": [ - { - \"role\": \"system\", - \"content\": \"$full_prompt\" - }, - { - \"role\": \"user\", - \"content\": \"$input\" - } - ] - }" - response=$(curl "https://${openai_api_url}/v1/chat/completions" \ - --silent \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $OPENAI_API_KEY" \ - -d "$data") - response_code=$? - - if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then - echo "{\"date\":\"$(date)\",\"log\":\"Called OpenAI API\",\"input\":\"$input\",\"response\":\"$response\",\"response_code\":\"$response_code\"}" >> /tmp/zsh-copilot.log - fi - - if [[ $response_code -ne 0 ]]; then - echo "Error fetching suggestions from the OpenAI API. Please check your API key and try again." > /tmp/.zsh_copilot_error - return 1 - fi + local input="$1" + local full_prompt="$2" + local provider_func="_zsh_copilot_provider_${ZSH_COPILOT_AI_PROVIDER}" - message=$(echo "$response" | tr -d '\n' | jq -r '.choices[0].message.content') - elif [[ "$ZSH_COPILOT_AI_PROVIDER" == "anthropic" ]]; then - # Anthropic's API payload - data="{ - \"model\": \"claude-3-5-sonnet-latest\", - \"max_tokens\": 1000, - \"system\": \"$full_prompt\", - \"messages\": [ - { - \"role\": \"user\", - \"content\": \"$input\" - } - ] - }" - response=$(curl "https://${anthropic_api_url}/v1/messages" \ - --silent \ - -H "Content-Type: application/json" \ - -H "x-api-key: $ANTHROPIC_API_KEY" \ - -H "anthropic-version: 2023-06-01" \ - -d "$data") - response_code=$? - - if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then - echo "{\"date\":\"$(date)\",\"log\":\"Called Anthropic API\",\"input\":\"$input\",\"response\":\"$response\",\"response_code\":\"$response_code\"}" >> /tmp/zsh-copilot.log - fi - - if [[ $response_code -ne 0 ]] || [[ $(echo "$response" | jq -r '.type') == 'error' ]]; then - if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then - echo "{\"date\":\"$(date)\",\"log\":\"Error fetching Anthropic\"}" >> /tmp/zsh-copilot.log - fi - - echo "Error fetching suggestions from the Anthropic API. Please check your API key and try again." > /tmp/.zsh_copilot_error - return 1 - fi - - message=$(echo "$response" | tr -d '\n' | jq -r '.content[0].text') - else - echo "Invalid AI provider selected. Please choose 'openai' or 'anthropic'." + if (( ! $+functions[$provider_func] )); then + echo "Unknown provider: $ZSH_COPILOT_AI_PROVIDER" > /tmp/.zsh_copilot_error return 1 fi - echo "$message" > /tmp/zsh_copilot_suggestion || return 1 + $provider_func "$input" "$full_prompt" } @@ -168,23 +91,16 @@ function _show_loading_animation() { } function _suggest_ai() { - #### Prepare environment - local openai_api_url=${OPENAI_API_URL:-"api.openai.com"} - local anthropic_api_url=${ANTHROPIC_API_URL:-"api.anthropic.com"} - local context_info="" if [[ "$ZSH_COPILOT_SEND_CONTEXT" == 'true' ]]; then - local system + # Iterate over all context plugins and chain their output + for context_func in ${(k)functions[(I)_zsh_copilot_context_*]}; do + context_info=$($context_func "$context_info") + done - if [[ "$OSTYPE" == "darwin"* ]]; then - system="Your system is ${$(sw_vers | xargs | sed 's/ /./g')}." - else - system="Your system is ${$(cat /etc/*-release | xargs | sed 's/ /,/g')}." + if [[ -n "$context_info" ]]; then + context_info="Context: $context_info" fi - - context_info="Context: You are user $(whoami) with id $(id) in directory $(pwd). - Your shell is $(echo $SHELL) and your terminal is $(echo $TERM) running on $(uname -a). - $system" fi ##### Get input @@ -197,14 +113,14 @@ function _suggest_ai() { local full_prompt=$(echo "$ZSH_COPILOT_SYSTEM_PROMPT $context_info" | tr -d '\n') ##### Fetch message - read < <(_fetch_suggestions & echo $!) + read < <(_fetch_suggestions $input $full_prompt & echo $!) local pid=$REPLY _show_loading_animation $pid local response_code=$? if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then - echo "{\"date\":\"$(date)\",\"log\":\"Fetched message\",\"input\":\"$input\",\"response\":\"$response_code\",\"message\":\"$message\"}" >> /tmp/zsh-copilot.log + echo "{\"date\":\"$(date)\",\"log\":\"Fetched message\",\"input\":\"$input\",\"response_code\":\"$response_code\",\"message\":\"$message\"}" >> /tmp/zsh-copilot.log fi if [[ ! -f /tmp/zsh_copilot_suggestion ]]; then @@ -215,36 +131,41 @@ function _suggest_ai() { local message=$(cat /tmp/zsh_copilot_suggestion) - ##### Process response + ##### Process response - apply transform plugins + for transform_func in ${(k)functions[(I)_zsh_copilot_transform_*]}; do + message=$($transform_func "$message") + done - local first_char=${message:0:1} - local suggestion=${message:1:${#message}} - if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then - echo "{\"date\":\"$(date)\",\"log\":\"Suggestion extracted.\",\"input\":\"$input\",\"response\":\"$response\",\"first_char\":\"$first_char\",\"suggestion\":\"$suggestion\",\"data\":\"$data\"}" >> /tmp/zsh-copilot.log + echo "{\"date\":\"$(date)\",\"log\":\"Suggestion extracted.\",\"input\":\"$input\",\"response\":\"$response\",\"message\":\"$message\",\"data\":\"$data\"}" >> /tmp/zsh-copilot.log fi ##### And now, let's actually show the suggestion to the user! - if [[ "$first_char" == '=' ]]; then - # Reset user input - BUFFER="" - CURSOR=0 - - zle -U "$suggestion" - elif [[ "$first_char" == '+' ]]; then - _zsh_autosuggest_suggest "$suggestion" - fi + # Set the new suggestion (setting BUFFER directly allows undoing the whole text at once) + BUFFER="$message" + CURSOR=${#message} } function zsh-copilot() { + local provider_func="_zsh_copilot_provider_${ZSH_COPILOT_AI_PROVIDER}" + echo "ZSH Copilot is now active. Press $ZSH_COPILOT_KEY to get suggestions." + echo "" + echo "Current provider: $ZSH_COPILOT_AI_PROVIDER" + if (( $+functions[$provider_func] )); then + echo " Status: Provider loaded" + else + echo " Status: ERROR - Provider not found!" + fi + echo "" echo "Configurations:" - echo " - ZSH_COPILOT_KEY: Key to press to get suggestions (default: ^z, value: $ZSH_COPILOT_KEY)." - echo " - ZSH_COPILOT_SEND_CONTEXT: If \`true\`, zsh-copilot will send context information (whoami, shell, pwd, etc.) to the AI model (default: true, value: $ZSH_COPILOT_SEND_CONTEXT)." - echo " - ZSH_COPILOT_AI_PROVIDER: AI provider to use ('openai' or 'anthropic', value: $ZSH_COPILOT_AI_PROVIDER)." - echo " - ZSH_COPILOT_SYSTEM_PROMPT: System prompt to use for the AI model (uses a built-in prompt by default)." + echo " - ZSH_COPILOT_KEY: Key to press to get suggestions (default: ^z, value: $ZSH_COPILOT_KEY)" + echo " - ZSH_COPILOT_SEND_CONTEXT: Send context info to AI (default: true, value: $ZSH_COPILOT_SEND_CONTEXT)" + echo " - ZSH_COPILOT_AI_PROVIDER: AI provider to use (value: $ZSH_COPILOT_AI_PROVIDER)" + echo " - ZSH_COPILOT_PLUGINS_DIR: Custom plugins directory (default: ~/.zsh-copilot/plugins)" + echo " - ZSH_COPILOT_SYSTEM_PROMPT: Custom system prompt (uses built-in by default)" } zle -N _suggest_ai