diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..29c01fa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release Binaries + +on: + push: + tags: + - "v*" + +jobs: + build: + name: Build and Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.21" + + - name: Set release version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Build binaries + run: | + # Create a directory for binaries + mkdir -p ./dist + + # Build for Windows + GOOS=windows GOARCH=amd64 go build -o ./dist/shell-ai-${{ env.RELEASE_VERSION }}-windows-amd64.exe main.go + + # Build for macOS + GOOS=darwin GOARCH=amd64 go build -o ./dist/shell-ai-${{ env.RELEASE_VERSION }}-darwin-amd64 main.go + GOOS=darwin GOARCH=arm64 go build -o ./dist/shell-ai-${{ env.RELEASE_VERSION }}-darwin-arm64 main.go + + # Build for Linux + GOOS=linux GOARCH=amd64 go build -o ./dist/shell-ai-${{ env.RELEASE_VERSION }}-linux-amd64 main.go + GOOS=linux GOARCH=arm64 go build -o ./dist/shell-ai-${{ env.RELEASE_VERSION }}-linux-arm64 main.go + + # Make binaries executable + chmod +x ./dist/* + + # Create checksums + cd ./dist && sha256sum * > checksums.txt + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: "ShellAI ${{ env.RELEASE_VERSION }}" + draft: false + prerelease: false + generate_release_notes: true + files: | + ./dist/* diff --git a/README.md b/README.md index 2b8025b..705cf81 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ A delightfully minimal, yet remarkably powerful AI Shell Assistant. # About -For developers, referencing things online is inevitable – but one can only look up "how to do [X] in git" so many times before losing your mind. +For developers, referencing things online is inevitable – but one can only look up "how to do [X] in git" so many times before losing your mind.

@@ -31,14 +31,93 @@ _New: ShellAI now supports local models! See [Custom Model Configuration](#custo ### Homebrew ```bash -brew tap ibigio/tap +brew tap erfannf/tap brew install shell-ai ``` -### Linux +### Linux/macOS ```bash -curl https://raw.githubusercontent.com/ibigio/shell-ai/main/install.sh | bash +curl https://raw.githubusercontent.com/erfannf/shell-ai/main/install.sh | bash +``` + +### Windows + +```powershell +Invoke-RestMethod -Uri https://raw.githubusercontent.com/erfannf/shell-ai/main/install.ps1 | powershell -Command - +``` + +### SSH Session Requirements + +If you're using ShellAI over an SSH session, you'll need to install clipboard utilities for the copy functionality to work: + +- **On Debian/Ubuntu-based systems**: + ```bash + sudo apt-get install xsel xclip + ``` + +- **On Fedora/RHEL-based systems**: + ```bash + sudo dnf install xsel xclip + ``` + +- **On Arch Linux**: + ```bash + sudo pacman -S xsel xclip + ``` + +- **For Wayland users**: + ```bash + sudo apt-get install wl-clipboard # Debian/Ubuntu + sudo dnf install wl-clipboard # Fedora + sudo pacman -S wl-clipboard # Arch + ``` + +- **For Termux users**: + ```bash + pkg install termux-api + ``` + +Without these utilities, you'll see an error when attempting to copy text to clipboard. + +#### TTY Session Type Compatibility + +Clipboard functionality depends on your terminal session type: + +- **Local terminal sessions**: Clipboard should work without additional configuration. +- **X11 forwarding**: Use `ssh -X` or `ssh -Y` to connect to your server to enable clipboard sharing between the server and your local machine. + ```bash + ssh -X user@host + ``` +- **SSH without X forwarding**: Clipboard functionality will be limited to the remote server's clipboard only, not your local machine. +- **TTY sessions (no GUI)**: Clipboard functionality won't work as these sessions don't have access to a GUI clipboard. +- **tmux/screen sessions**: May require additional configuration: + ```bash + # For tmux, ensure this in your ~/.tmux.conf + set -g set-clipboard on + ``` + +If you're accessing a remote server and need to copy command output to your local machine, consider piping to a file and using `scp` or `rsync` to transfer it. + +# Uninstall + +### Homebrew + +```bash +brew uninstall shell-ai +brew untap erfannf/tap +``` + +### Linux/macOS + +```bash +rm -f /usr/local/bin/q +``` + +### Windows + +```powershell +Remove-Item -Path "$env:LOCALAPPDATA\Programs\shell-ai\q.exe" -Force ``` # Usage @@ -58,13 +137,43 @@ Type `q` followed by a description of a shell command, code snippet, or general ### Configuration -Set your [OpenAI API key](https://platform.openai.com/account/api-keys). +#### Setting up API keys + +Set your [OpenAI API key](https://platform.openai.com/account/api-keys): ```bash -export OPENAI_API_KEY=[your key] +export OPENAI_API_KEY="your-api-key-here" ``` -For more options (like setting the default model), run: +For persistent configuration, add the export command to your shell profile: + +- **Bash users** - Add to `~/.bashrc` or `~/.bash_profile`: + ```bash + echo 'export OPENAI_API_KEY="your-api-key-here"' >> ~/.bashrc + source ~/.bashrc + ``` + +- **Zsh users** - Add to `~/.zshrc`: + ```bash + echo 'export OPENAI_API_KEY="your-api-key-here"' >> ~/.zshrc + source ~/.zshrc + ``` + +- **Fish users** - Add to `~/.config/fish/config.fish`: + ```fish + set -Ux OPENAI_API_KEY "your-api-key-here" + ``` + +- **Windows PowerShell users** - Add to your PowerShell profile: + ```powershell + [Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "your-api-key-here", "User") + ``` + +> **Important:** Never include the square brackets when setting your API key. Use the format `export OPENAI_API_KEY="sk-abcd1234"` not `export OPENAI_API_KEY=[sk-abcd1234]`. + +#### Changing default model + +For more configuration options (like setting the default model), run: ```bash q config @@ -118,7 +227,7 @@ You can now configure model prompts and even add your own model setups in the `~ ### Config File Syntax -````yaml +```yaml preferences: default_model: gpt-4-1106-preview @@ -139,7 +248,7 @@ models: # other models ... config_format_version: "1" -```` +``` **Note:** The `auth_env_var` is set to `OPENAI_API_KEY` verbatim, not the key itself, so as to not keep sensitive information in the config file. @@ -159,7 +268,7 @@ Here's what I did: 4. Finally I added the new `model` config to my `~/.shell-ai/config.yaml`, and wrestled with the prompt until it worked – bet you can do better. (As you can see, YAML is [flexible](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html).) -````yaml +```yaml models: - name: stablelm-zephyr-3b.Q8_0 endpoint: http://127.0.0.1:8080/v1/chat/completions @@ -191,7 +300,7 @@ models: ``` # other models ... -```` +``` and also updated the default model (which you can also do from `q config`): @@ -204,6 +313,37 @@ And huzzah! You can now use ShellAI on a plane. (Fun fact, I implemented a good bit of the initial config TUI on a plane using this exact local model.) +### Setting Up DeepSeek Models + +ShellAI now supports DeepSeek's powerful LLMs via their API. To use DeepSeek models: + +1. Create an account at [DeepSeek Platform](https://platform.deepseek.com) +2. Generate an API key at https://platform.deepseek.com/api_keys +3. Set your DeepSeek API key as an environment variable: + +```bash +# For Linux/macOS (temporary) +export DEEPSEEK_API_KEY="your-api-key-here" + +# For permanent setup on Linux/macOS +echo 'export DEEPSEEK_API_KEY="your-api-key-here"' >> ~/.bashrc # for bash +# or +echo 'export DEEPSEEK_API_KEY="your-api-key-here"' >> ~/.zshrc # for zsh + +# For Windows (PowerShell) +$env:DEEPSEEK_API_KEY = "your-api-key-here" # temporary +# For permanent setup on Windows +[Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY", "your-api-key-here", "User") +``` + +> **Important:** Do not include square brackets around your API key. The correct format is `export DEEPSEEK_API_KEY="your-key"`, not `export DEEPSEEK_API_KEY=[your-key]`. + +4. Run `q config` and select "Change Default Model" to switch to one of the DeepSeek models: + - `deepseek-chat`: General-purpose conversational model + - `deepseek-coder`: Specialized for code and programming tasks + +The DeepSeek models are already configured in ShellAI, so you don't need to modify your config file manually. + ### Setting Up Azure OpenAI endpoint Define `AZURE_OPENAI_API_KEY` environment variable and make few changes to the config file. diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..4948e06 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,67 @@ +$VERSION = "v1.2.3" +$PLATFORMS = @( + @{GOOS = "windows"; GOARCH = "amd64"; ARCH_NAME = "x86_64"; Ext = ".exe" }, + @{GOOS = "windows"; GOARCH = "386"; ARCH_NAME = "x86"; Ext = ".exe" }, + @{GOOS = "linux"; GOARCH = "amd64"; ARCH_NAME = "x86_64"; Ext = "" }, + @{GOOS = "linux"; GOARCH = "386"; ARCH_NAME = "x86"; Ext = "" }, + @{GOOS = "linux"; GOARCH = "arm64"; ARCH_NAME = "arm64"; Ext = "" }, + @{GOOS = "darwin"; GOARCH = "amd64"; ARCH_NAME = "x86_64"; Ext = "" }, + @{GOOS = "darwin"; GOARCH = "arm64"; ARCH_NAME = "arm64"; Ext = "" } +) + +# Create dist directory if it doesn't exist +if (-not (Test-Path -Path "dist")) { + New-Item -ItemType Directory -Path "dist" | Out-Null +} + +foreach ($platform in $PLATFORMS) { + $env:GOOS = $platform.GOOS + $env:GOARCH = $platform.GOARCH + + # Create the filename compatible with install scripts + $outFile = "dist/shell-ai_$($platform.GOOS)_$($platform.ARCH_NAME)$($platform.Ext)" + + Write-Host "Building for $($platform.GOOS)/$($platform.GOARCH)..." + go build -o $outFile main.go + + if ($LASTEXITCODE -eq 0) { + Write-Host " Success: $outFile" + + # Create tar.gz archive for non-Windows platforms + if ($platform.GOOS -ne "windows") { + $tarName = "shell-ai_$($platform.GOOS)_$($platform.ARCH_NAME).tar.gz" + + # The commands below will work on Windows with tar installed + # Create a temporary directory for the binary + $tempDir = "dist/temp_$($platform.GOOS)_$($platform.ARCH_NAME)" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + # Copy the binary to the temp directory with the simple name + Copy-Item -Path $outFile -Destination "$tempDir/shell-ai" + + # Create tar.gz archive + Set-Location $tempDir + tar -czf "../$tarName" "shell-ai" + Set-Location "../.." + + # Cleanup temp directory + Remove-Item -Path $tempDir -Recurse -Force + + Write-Host " Created archive: dist/$tarName" + } + } + else { + Write-Host " Failed to build for $($platform.GOOS)/$($platform.GOARCH)" -ForegroundColor Red + } +} + +# Create checksums file +Set-Location dist +$checksums = Get-ChildItem -File | Where-Object { $_.Name -ne "checksums.txt" } | ForEach-Object { + $hash = Get-FileHash -Path $_.FullName -Algorithm SHA256 + "$($hash.Hash.ToLower()) $($_.Name)" +} +$checksums | Out-File -FilePath "checksums.txt" -Encoding utf8 + +Write-Host "`nBuild completed. Binaries are in the dist/ directory." +Write-Host "SHA256 checksums have been saved to dist/checksums.txt" \ No newline at end of file diff --git a/cli/cli.go b/cli/cli.go index ea4a744..2d402e6 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -129,18 +129,23 @@ func (m model) getConnectionError(err error) string { styleRed := lipgloss.NewStyle().Foreground(lipgloss.Color("9")) styleGreen := lipgloss.NewStyle().Foreground(lipgloss.Color("2")) styleDim := lipgloss.NewStyle().Faint(true).Width(m.maxWidth).PaddingLeft(2) + + // Create a more generic error message + errorMsg := "Error: Failed to connect to the API service." + message := fmt.Sprintf("\n %v\n\n%v\n", - styleRed.Render("Error: Failed to connect to OpenAI."), + styleRed.Render(errorMsg), styleDim.Render(err.Error())) + + // Add information for rate limit errors if util.IsLikelyBillingError(err.Error()) { - message = fmt.Sprintf("%v\n %v %v\n\n %v%v\n\n", + message = fmt.Sprintf("%v\n %v %v\n\n", message, styleGreen.Render("Hint:"), - "You may need to set up billing. You can do so here:", - styleGreen.Render("->"), - styleDim.Render("https://platform.openai.com/account/billing"), + "You may have reached your rate limit. Check your API usage and billing status.", ) } + return message } @@ -322,6 +327,24 @@ func printAPIKeyNotSetMessage(modelConfig ModelConfig) { %s 4. (Recommended) Add that ^ line to your %s file.`, shellSyntax, profileScriptName) + msg2, _ := r.Render(message_string) + fmt.Printf("\n %v%v\n", msg1, msg2) + case "DEEPSEEK_API_KEY": + msg1 := styleRed.Render("DEEPSEEK_API_KEY environment variable not set.") + + deepseekSyntax := shellSyntax + if runtime.GOOS == "windows" { + deepseekSyntax = "\n```powershell\n$env:DEEPSEEK_API_KEY = \"[your key]\"\n```" + } else { + deepseekSyntax = "\n```bash\nexport DEEPSEEK_API_KEY=[your key]\n```" + } + + message_string := fmt.Sprintf(` + 1. Generate your API key at https://platform.deepseek.com/api_keys + 2. Set your key by running: + %s + 3. (Recommended) Add that ^ line to your %s file.`, deepseekSyntax, profileScriptName) + msg2, _ := r.Render(message_string) fmt.Printf("\n %v%v\n", msg1, msg2) default: diff --git a/config/config.yaml b/config/config.yaml index 0494480..f7e611e 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -29,5 +29,27 @@ models: content: print hi - role: assistant content: "```bash\necho \"hi\"\n```" + + - name: deepseek-chat + endpoint: https://api.deepseek.com/v1/chat/completions + auth_env_var: DEEPSEEK_API_KEY + prompt: + - role: system + content: You are a terminal assistant. Turn the natural language instructions into a terminal command. By default always only output code, and in a code block. However, if the user is clearly asking a question then answer it very briefly and well. Consider when the user request references a previous request. + - role: user + content: print hi + - role: assistant + content: "```bash\necho \"hi\"\n```" + + - name: deepseek-coder + endpoint: https://api.deepseek.com/v1/chat/completions + auth_env_var: DEEPSEEK_API_KEY + prompt: + - role: system + content: You are a coding assistant. Always output code in a code block with the appropriate language tag. Provide concise solutions to coding problems. If explaining code, be brief. Consider when the user request references a previous request. + - role: user + content: write a recursive fibonacci function in python + - role: assistant + content: "```python\ndef fibonacci(n):\n if n <= 1:\n return n\n else:\n return fibonacci(n-1) + fibonacci(n-2)\n```" config_format_version: "1" diff --git a/install.ps1 b/install.ps1 index e6f6ab3..a6e7ded 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,5 +1,5 @@ param( - [string]$repoowner = "ibigio", + [string]$repoowner = "erfannf", [string]$reponame = "shell-ai", [string]$toolname = "shell-ai", [string]$toolsymlink = "q", @@ -12,7 +12,7 @@ if ($help) { Write-Host " shell-ai -help " Write-Host " shell-ai -repoowner " Write-Host " shell-ai -reponame " - Write-Host " shell-ai -toolname " + Write-Host " shell-ai -toolname " Write-Host " shell-ai -toolsymlink " exit 0 @@ -32,19 +32,21 @@ if (-not (IsUserAdministrator)) { # Detect the platform (architecture and OS) $ARCH = $null -$OS = "Windows" - +$OS = "windows" # Lowercase to match install.sh convention if ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") { - $ARCH = "x86_64" -} elseif ($env:PROCESSOR_ARCHITECTURE -eq "arm64") { + $ARCH = "x86_64" # Match the architecture naming in install.sh +} +elseif ($env:PROCESSOR_ARCHITECTURE -eq "arm64") { $ARCH = "arm64" -} else { +} +else { $ARCH = "i386" } if ($env:OS -notmatch "Windows") { - Write-Host "You are running the powershell script on a non-windows platform. Please use the install.sh script instead." + Write-Host "You are running the PowerShell script on a non-Windows platform. Please use the install.sh script instead." + exit 1 } # Fetch the latest release tag from GitHub API @@ -52,45 +54,49 @@ $API_URL = "https://api.github.com/repos/$repoowner/$reponame/releases/latest" $LATEST_TAG = (Invoke-RestMethod -Uri $API_URL).tag_name # Set the download URL based on the platform and latest release tag -$DOWNLOAD_URL = "https://github.com/$repoowner/$reponame/releases/download/$LATEST_TAG/${toolname}_${OS}_${ARCH}.zip" +# Using the same naming convention as install.sh +$DOWNLOAD_URL = "https://github.com/$repoowner/$reponame/releases/download/$LATEST_TAG/${toolname}_${OS}_${ARCH}.tar.gz" -Write-Host $DOWNLOAD_URL +Write-Host "Downloading from: $DOWNLOAD_URL" -# Download the ZIP file -Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile "${toolname}.zip" +# Create a temporary directory +$tempDir = New-Item -ItemType Directory -Force -Path "$env:TEMP\$toolname-temp" -# Extract the ZIP file -$extractedDir = "${toolname}-temp" -Expand-Archive -Path "${toolname}.zip" -DestinationPath $extractedDir -Force +# Download the tar.gz file +Invoke-WebRequest -Uri $DOWNLOAD_URL -OutFile "$tempDir\$toolname.tar.gz" -# check if the file already exists -$toolPath = "C:\Program Files\shell-ai\${toolsymlink}.exe" -if (Test-Path $toolPath) { - Remove-Item $toolPath -} else { - New-Item -ItemType Directory -Path "C:\Program Files\shell-ai\" -} +# Extract the tar.gz file using Windows built-in tar (Windows 10 1803+) +# First change to the temp directory +Push-Location $tempDir +tar -xzf "$toolname.tar.gz" +Pop-Location -# Add the file to path -$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", "User") +# Create installation directory if it doesn't exist +$installDir = "C:\Program Files\$toolname" +if (-not (Test-Path $installDir)) { + New-Item -ItemType Directory -Path $installDir | Out-Null +} -# Append the desired path to the current PATH value if it's not already present -if (-not ($currentPath -split ";" | Select-String -SimpleMatch "C:\Program Files\shell-ai\")) { - $updatedPath = $currentPath + ";" + "C:\Program Files\shell-ai\" +# Check if the file already exists and remove it +$toolPath = "$installDir\$toolsymlink.exe" +if (Test-Path $toolPath) { + Remove-Item $toolPath -Force +} - # Set the updated PATH value - [System.Environment]::SetEnvironmentVariable("PATH", $updatedPath, "User") # Use "User" instead of "Machine" for user-level PATH +# Copy the executable to the installation directory +Move-Item -Path "$tempDir\$toolname" -Destination $toolPath -Force - Write-Host "The path has been added to the PATH variable. You may need to restart applications to see the changes." -ForegroundColor Red +# Add the installation directory to PATH if not already present +$currentPath = [System.Environment]::GetEnvironmentVariable("PATH", "User") +if (-not ($currentPath -split ";" | Select-String -SimpleMatch $installDir)) { + $updatedPath = $currentPath + ";" + $installDir + [System.Environment]::SetEnvironmentVariable("PATH", $updatedPath, "User") + Write-Host "The installation directory has been added to your PATH. You may need to restart your terminal to use the command." -ForegroundColor Yellow } -# Make the binary executable -Move-Item "${extractedDir}/${toolname}.exe" $toolPath -Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted - # Clean up -Remove-Item -Recurse -Force "${extractedDir}" -Remove-Item -Force "${toolname}.zip" +Remove-Item -Recurse -Force $tempDir # Print success message -Write-Host "The $toolname has been installed successfully (version: $LATEST_TAG)." \ No newline at end of file +Write-Host "The $toolname has been installed successfully (version: $LATEST_TAG)." -ForegroundColor Green +Write-Host "You can now use '$toolsymlink' from your terminal." -ForegroundColor Green \ No newline at end of file diff --git a/install.sh b/install.sh index 5346d57..cd61877 100644 --- a/install.sh +++ b/install.sh @@ -1,7 +1,7 @@ #!/bin/bash # Replace these values with your tool's information: -REPO_OWNER="ibigio" +REPO_OWNER="erfannf" REPO_NAME="shell-ai" TOOL_NAME="shell-ai" TOOL_SYMLINK="q" diff --git a/llm/llm.go b/llm/llm.go index 35e43e5..439ee84 100644 --- a/llm/llm.go +++ b/llm/llm.go @@ -42,6 +42,8 @@ func (c *LLMClient) createRequest(payload Payload) (*http.Request, error) { } if strings.Contains(c.config.Endpoint, "openai.azure.com") { req.Header.Set("Api-Key", c.config.Auth) + } else if strings.Contains(c.config.Endpoint, "api.deepseek.com") { + req.Header.Set("Authorization", "Bearer "+c.config.Auth) } else { req.Header.Set("Authorization", "Bearer "+c.config.Auth) }