From 8bae0dee13d139874efd7dc9cd775a5c5eae2a26 Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 08:41:58 +0100 Subject: [PATCH 1/6] fix: Small improvements --- _debug.zsh | 5 ++++ zsh-copilot.plugin.zsh | 59 ++++++++++++++++++++++-------------------- 2 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 _debug.zsh 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/zsh-copilot.plugin.zsh b/zsh-copilot.plugin.zsh index 867f60a..375bbbf 100644 --- a/zsh-copilot.plugin.zsh +++ b/zsh-copilot.plugin.zsh @@ -27,15 +27,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 +37,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 @@ -61,6 +54,8 @@ function _fetch_suggestions() { local response local message + local input=$1 + if [[ "$ZSH_COPILOT_AI_PROVIDER" == "openai" ]]; then # OpenAI's API payload data="{ @@ -96,8 +91,8 @@ function _fetch_suggestions() { elif [[ "$ZSH_COPILOT_AI_PROVIDER" == "anthropic" ]]; then # Anthropic's API payload data="{ - \"model\": \"claude-3-5-sonnet-latest\", - \"max_tokens\": 1000, + \"model\": \"claude-3-haiku-20240307\", + \"max_tokens\": 500, \"system\": \"$full_prompt\", \"messages\": [ { @@ -197,14 +192,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 & 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 @@ -217,27 +212,35 @@ function _suggest_ai() { ##### Process response - 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 + # Reset user input + BUFFER="" + CURSOR=0 - zle -U "$suggestion" - elif [[ "$first_char" == '+' ]]; then - _zsh_autosuggest_suggest "$suggestion" - fi + zle -U "$message" } function zsh-copilot() { + # Check if openai is selected and API key is set + if [[ "$ZSH_COPILOT_AI_PROVIDER" == "openai" && -n "$OPENAI_API_KEY" ]]; then + echo "OpenAI is selected as AI provider and the key is set." + elif [[ "$ZSH_COPILOT_AI_PROVIDER" == "anthropic" && -n "$ANTHROPIC_API_KEY" ]]; then + echo "Anthropic is selected as AI provider and the key is set." + else + if [[ -z"$ZSH_COPILOT_AI_PROVIDER" ]]; then + echo "Error: No AI provider selected. Please set ZSH_COPILOT_AI_PROVIDER to 'openai' or 'anthropic'." + exit 1 + else + echo "Error: API key for $ZSH_COPILOT_AI_PROVIDER is not set. Please set the appropriate environment variable." + exit 1 + fi + fi + echo "ZSH Copilot is now active. Press $ZSH_COPILOT_KEY to get suggestions." echo "" echo "Configurations:" From f905910aa7173feda191ac1baf404efd599b9437 Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 08:55:40 +0100 Subject: [PATCH 2/6] fix: Make undo restore entire suggestion at once Previously used `zle -U` which inserted characters one at a time, requiring multiple undo operations. Now sets BUFFER directly so Ctrl+/ undoes the whole AI suggestion in one step. Co-Authored-By: Claude Opus 4.5 --- zsh-copilot.plugin.zsh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/zsh-copilot.plugin.zsh b/zsh-copilot.plugin.zsh index 375bbbf..9d135d4 100644 --- a/zsh-copilot.plugin.zsh +++ b/zsh-copilot.plugin.zsh @@ -218,11 +218,9 @@ function _suggest_ai() { ##### And now, let's actually show the suggestion to the user! - # Reset user input - BUFFER="" - CURSOR=0 - - zle -U "$message" + # Set the new suggestion (setting BUFFER directly allows undoing the whole text at once) + BUFFER="$message" + CURSOR=${#message} } function zsh-copilot() { From e42f93fea138d1595f9fbc00633355b53fa97f1d Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 09:15:16 +0100 Subject: [PATCH 3/6] feat: Add plugin system for AI providers Refactor provider logic into separate plugin files in plugins/providers/. Users can now create custom providers by defining _zsh_copilot_provider_ functions. Built-in providers for OpenAI and Anthropic included. Co-Authored-By: Claude Opus 4.5 --- README.md | 44 ++++++++++-- plugins/providers/anthropic.zsh | 53 ++++++++++++++ plugins/providers/openai.zsh | 50 +++++++++++++ zsh-copilot.plugin.zsh | 122 ++++++-------------------------- 4 files changed, 163 insertions(+), 106 deletions(-) create mode 100644 plugins/providers/anthropic.zsh create mode 100644 plugins/providers/openai.zsh diff --git a/README.md b/README.md index ee40078..7ba789b 100644 --- a/README.md +++ b/README.md @@ -42,29 +42,35 @@ echo "source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh" >> ~/.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: +### Setting up a Provider -For OpenAI (default): +zsh-copilot uses a plugin system for AI providers. You need to load a provider before using the plugin. + +**Using OpenAI:** ```sh export OPENAI_API_KEY= +zsh-copilot-load-provider openai ``` -For Anthropic: +**Using Anthropic:** ```sh export ANTHROPIC_API_KEY= +zsh-copilot-load-provider anthropic ``` -You can configure the AI provider using the `ZSH_COPILOT_AI_PROVIDER` variable: +Add these lines to your `.zshrc` after sourcing zsh-copilot. +You can also set the provider explicitly: ```sh export ZSH_COPILOT_AI_PROVIDER="openai" # or "anthropic" ``` -Other configuration options: +### Other Options - `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) +- `ZSH_COPILOT_PLUGINS_DIR`: Custom plugins directory (default: ~/.zsh-copilot/plugins) To see all available configurations and their current values, run: @@ -72,6 +78,34 @@ To see all available configurations and their current values, run: zsh-copilot ``` +## Custom Providers + +You can create your own AI provider by defining a function following this pattern: + +```sh +# ~/.zsh-copilot/plugins/providers/ollama.zsh +_zsh_copilot_provider_ollama() { + local input="$1" + local system_prompt="$2" + + # Your implementation here... + # Must write result to /tmp/zsh_copilot_suggestion + # Must write errors to /tmp/.zsh_copilot_error + # Return 0 on success, 1 on failure + + local response=$(curl http://localhost:11434/api/generate \ + --silent \ + -d "{\"model\": \"llama2\", \"prompt\": \"$system_prompt\n\nUser: $input\", \"stream\": false}" \ + | jq -r '.response') + + echo "$response" > /tmp/zsh_copilot_suggestion +} +``` + +Then set `ZSH_COPILOT_AI_PROVIDER=ollama` and your custom provider will be used. + +Files in `~/.zsh-copilot/plugins/providers/` are automatically loaded. + ## Usage Type in your command or your message and press `CTRL + Z` to get your suggestion! 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/zsh-copilot.plugin.zsh b/zsh-copilot.plugin.zsh index 9d135d4..eba4268 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 @@ -50,85 +47,16 @@ if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then fi function _fetch_suggestions() { - local data - local response - local message - - local input=$1 - - 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-haiku-20240307\", - \"max_tokens\": 500, - \"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" } @@ -163,10 +91,6 @@ 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 @@ -192,7 +116,7 @@ function _suggest_ai() { local full_prompt=$(echo "$ZSH_COPILOT_SYSTEM_PROMPT $context_info" | tr -d '\n') ##### Fetch message - read < <(_fetch_suggestions $input & echo $!) + read < <(_fetch_suggestions $input $full_prompt & echo $!) local pid=$REPLY _show_loading_animation $pid @@ -224,28 +148,24 @@ function _suggest_ai() { } function zsh-copilot() { - # Check if openai is selected and API key is set - if [[ "$ZSH_COPILOT_AI_PROVIDER" == "openai" && -n "$OPENAI_API_KEY" ]]; then - echo "OpenAI is selected as AI provider and the key is set." - elif [[ "$ZSH_COPILOT_AI_PROVIDER" == "anthropic" && -n "$ANTHROPIC_API_KEY" ]]; then - echo "Anthropic is selected as AI provider and the key is set." + 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 - if [[ -z"$ZSH_COPILOT_AI_PROVIDER" ]]; then - echo "Error: No AI provider selected. Please set ZSH_COPILOT_AI_PROVIDER to 'openai' or 'anthropic'." - exit 1 - else - echo "Error: API key for $ZSH_COPILOT_AI_PROVIDER is not set. Please set the appropriate environment variable." - exit 1 - fi + echo " Status: ERROR - Provider not found!" fi - echo "ZSH Copilot is now active. Press $ZSH_COPILOT_KEY to get suggestions." 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 From 66259343f97ccdcaab1eea799ac13284cec0904c Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 09:21:30 +0100 Subject: [PATCH 4/6] feat: Add context and transform plugin systems - Context plugins add info to the prompt (e.g., system info, git status) - Transform plugins process AI responses (e.g., normalize whitespace/quotes) - Both plugin types chain together when multiple are loaded - Includes built-in plugins: context/system.zsh, transform/normalize.zsh Co-Authored-By: Claude Opus 4.5 --- README.md | 58 +++++++++++++++++++++++++++++++++ plugins/context/system.zsh | 25 ++++++++++++++ plugins/transform/normalize.zsh | 46 ++++++++++++++++++++++++++ zsh-copilot.plugin.zsh | 20 ++++++------ 4 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 plugins/context/system.zsh create mode 100644 plugins/transform/normalize.zsh diff --git a/README.md b/README.md index 7ba789b..ba8e3a4 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,64 @@ Then set `ZSH_COPILOT_AI_PROVIDER=ollama` and your custom provider will be used. Files in `~/.zsh-copilot/plugins/providers/` are automatically loaded. +## Context Plugins + +Context plugins add information to the prompt sent to the AI. Multiple context plugins can be loaded and they chain together - each plugin receives the output of previous plugins. + +Built-in context plugins: +- `system` - Adds user, shell, terminal, and OS information + +### Creating a Context Plugin + +```sh +# ~/.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 + git_info="You are in a git repository on branch $(git branch --show-current)." + fi + + if [[ -n "$context" ]]; then + echo "$context $git_info" + else + echo "$git_info" + fi +} +``` + +Context plugins must: +- Be named `_zsh_copilot_context_` +- Accept the current context as `$1` +- Echo the augmented context (original + additions) + +Files in `~/.zsh-copilot/plugins/context/` are automatically loaded. + +## Transform Plugins + +Transform plugins process the AI response before displaying it. Multiple transform plugins chain together. + +Built-in transform plugins: +- `normalize` - Trims whitespace, removes quotes, strips markdown code blocks + +### Creating a Transform Plugin + +```sh +# ~/.zsh-copilot/plugins/transform/lowercase.zsh +_zsh_copilot_transform_lowercase() { + local message="$1" + echo "${message:l}" +} +``` + +Transform plugins must: +- Be named `_zsh_copilot_transform_` +- Accept the message as `$1` +- Echo the transformed message + +Files in `~/.zsh-copilot/plugins/transform/` are automatically loaded. + ## Usage Type in your command or your message and press `CTRL + Z` to get your suggestion! 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/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 eba4268..adc7fd0 100644 --- a/zsh-copilot.plugin.zsh +++ b/zsh-copilot.plugin.zsh @@ -93,17 +93,14 @@ function _show_loading_animation() { function _suggest_ai() { 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 @@ -134,7 +131,10 @@ 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 if [[ "$ZSH_COPILOT_DEBUG" == 'true' ]]; then echo "{\"date\":\"$(date)\",\"log\":\"Suggestion extracted.\",\"input\":\"$input\",\"response\":\"$response\",\"message\":\"$message\",\"data\":\"$data\"}" >> /tmp/zsh-copilot.log From 161ecb5abaefd71f012094586d6f5d4d33e061f3 Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 09:26:42 +0100 Subject: [PATCH 5/6] docs: Clean up README and add PLUGINS.md - Simplify README with clear installation and plugin loading instructions - Recommend ~/.config/zsh-copilot installation path - Add PLUGINS.md with detailed documentation for creating custom plugins - Include examples for providers, context, and transform plugins Co-Authored-By: Claude Opus 4.5 --- PLUGINS.md | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 162 ++++++++++------------------------- 2 files changed, 291 insertions(+), 117 deletions(-) create mode 100644 PLUGINS.md 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 ba8e3a4..63d3601 100644 --- a/README.md +++ b/README.md @@ -10,161 +10,89 @@ 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 - -1. Clone `zsh-copilot` into `$ZSH_CUSTOM/plugins` (by default ~/.config/oh-my-zsh/custom/plugins) - -```sh -git clone https://git.myzel394.app/Myzel394/zsh-copilot ${ZSH_CUSTOM:-~/.config/oh-my-zsh/custom}/plugins/zsh-copilot -``` - -2. Add `zsh-copilot` to the plugins array in your `.zshrc` file: - -```bash -plugins=( - # your other plugins... - zsh-autosuggestions -) -``` +### Recommended Installation -### Manual Installation +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 ~/.config/zsh-copilot -echo "source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh" >> ~/.zshrc -``` - -## Configuration - -### Setting up a Provider - -zsh-copilot uses a plugin system for AI providers. You need to load a provider before using the plugin. - -**Using OpenAI:** -```sh -export OPENAI_API_KEY= -zsh-copilot-load-provider openai ``` -**Using Anthropic:** +Add to your `.zshrc`: ```sh -export ANTHROPIC_API_KEY= -zsh-copilot-load-provider anthropic +source ~/.config/zsh-copilot/zsh-copilot.plugin.zsh ``` -Add these lines to your `.zshrc` after sourcing zsh-copilot. +### Oh My Zsh -You can also set the provider explicitly: ```sh -export ZSH_COPILOT_AI_PROVIDER="openai" # or "anthropic" +git clone https://git.myzel394.app/Myzel394/zsh-copilot ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-copilot ``` -### Other Options +Add `zsh-copilot` to the plugins array in your `.zshrc`. + +## Configuration -- `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) -- `ZSH_COPILOT_PLUGINS_DIR`: Custom plugins directory (default: ~/.zsh-copilot/plugins) +### Loading a Provider -To see all available configurations and their current values, run: +You need to source a provider plugin. Add one of these to your `.zshrc` after sourcing zsh-copilot: +**OpenAI:** ```sh -zsh-copilot +export OPENAI_API_KEY= +source ~/.config/zsh-copilot/plugins/providers/openai.zsh ``` -## Custom Providers - -You can create your own AI provider by defining a function following this pattern: - +**Anthropic:** ```sh -# ~/.zsh-copilot/plugins/providers/ollama.zsh -_zsh_copilot_provider_ollama() { - local input="$1" - local system_prompt="$2" - - # Your implementation here... - # Must write result to /tmp/zsh_copilot_suggestion - # Must write errors to /tmp/.zsh_copilot_error - # Return 0 on success, 1 on failure - - local response=$(curl http://localhost:11434/api/generate \ - --silent \ - -d "{\"model\": \"llama2\", \"prompt\": \"$system_prompt\n\nUser: $input\", \"stream\": false}" \ - | jq -r '.response') - - echo "$response" > /tmp/zsh_copilot_suggestion -} +export ANTHROPIC_API_KEY= +source ~/.config/zsh-copilot/plugins/providers/anthropic.zsh ``` -Then set `ZSH_COPILOT_AI_PROVIDER=ollama` and your custom provider will be used. - -Files in `~/.zsh-copilot/plugins/providers/` are automatically loaded. - -## Context Plugins - -Context plugins add information to the prompt sent to the AI. Multiple context plugins can be loaded and they chain together - each plugin receives the output of previous plugins. +### Loading Plugins -Built-in context plugins: -- `system` - Adds user, shell, terminal, and OS information - -### Creating a Context Plugin +Source the plugins you want: ```sh -# ~/.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 - git_info="You are in a git repository on branch $(git branch --show-current)." - fi - - if [[ -n "$context" ]]; then - echo "$context $git_info" - else - echo "$git_info" - fi -} -``` +# Provider (required - pick one) +source ~/.config/zsh-copilot/plugins/providers/openai.zsh -Context plugins must: -- Be named `_zsh_copilot_context_` -- Accept the current context as `$1` -- Echo the augmented context (original + additions) +# Context plugins (optional - adds info to prompts) +source ~/.config/zsh-copilot/plugins/context/system.zsh -Files in `~/.zsh-copilot/plugins/context/` are automatically loaded. +# Transform plugins (optional - processes AI responses) +source ~/.config/zsh-copilot/plugins/transform/normalize.zsh +``` -## Transform Plugins +### Built-in Plugins -Transform plugins process the AI response before displaying it. Multiple transform plugins chain together. +Take a look at the [plugins directory](./plugins) for built-in plugins. -Built-in transform plugins: -- `normalize` - Trims whitespace, removes quotes, strips markdown code blocks +| 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 | +| Transform | `normalize` | Trims whitespace, removes quotes/code blocks | -### Creating a Transform Plugin +### Options -```sh -# ~/.zsh-copilot/plugins/transform/lowercase.zsh -_zsh_copilot_transform_lowercase() { - local message="$1" - echo "${message:l}" -} -``` - -Transform plugins must: -- Be named `_zsh_copilot_transform_` -- Accept the message as `$1` -- Echo the transformed message +| 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 | -Files in `~/.zsh-copilot/plugins/transform/` are automatically loaded. +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. From 8862dbccd0d198e4d68d74c7c8be5da194b1f49a Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Thu, 22 Jan 2026 09:30:49 +0100 Subject: [PATCH 6/6] feat: Add git context plugin Adds repository name, branch, and uncommitted changes count to the context. Co-Authored-By: Claude Opus 4.5 --- README.md | 1 + plugins/context/git.zsh | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 plugins/context/git.zsh diff --git a/README.md b/README.md index 63d3601..9f96827 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Take a look at the [plugins directory](./plugins) for built-in plugins. | 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 | ### Options 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 +}