├── .gitignore ├── LICENSE ├── README.md ├── assets ├── uwu.gif └── uwu.jpg ├── bun.lock ├── context.ts ├── index.ts ├── package.json ├── sample_configs ├── claude.json ├── clipboard.json ├── context.json ├── gemini.json ├── github.json ├── ollama.json └── openai.json ├── scripts └── test-uwu.sh └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Use Context, Inc. 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | uwu 4 |
5 | uwu 6 |
7 |

8 | 9 |

✨ Natural language to shell commands using AI ✨

10 | 11 |

12 | 13 | X (formerly Twitter) 14 | 15 | 16 | License 17 | 18 | 19 | GitHub 20 | 21 | 22 |

23 | 24 |

25 | What is this? • 26 | Installation • 27 | Usage • 28 | Contributing 29 |

30 | 31 | ## What is this? 32 | 33 | `uwu` is a lightweight, focused CLI tool that converts natural language into shell commands using Large Language Models (LLMs) like GPT-5. Unlike comprehensive agentic development tools like [Claude Code](https://www.anthropic.com/claude-code) or [Cursor](https://cursor.com), `uwu` has a simple, singular purpose: **helping you write shell commands faster, without switching context**. 34 | 35 | `uwu` is not a replacement for comprehensive agentic development tools -- it is simple tool that excels at one thing. Consider it the terminal equivalent of quickly searching "how do I..." and getting an immediately runnable answer. 36 | 37 | ![uwu demo](https://raw.githubusercontent.com/context-labs/uwu/main/assets/uwu.gif) 38 | 39 | After a response is generated, you can edit it before pressing enter to execute the command. This is useful if you want to add flags, or other modifications to the command. 40 | 41 | ## Installation 42 | 43 | ### 1. Clone the repo 44 | 45 | ```bash 46 | git clone https://github.com/context-labs/uwu.git 47 | cd uwu 48 | ``` 49 | 50 | ### 2. Install dependencies and build 51 | 52 | Make sure you have [Bun](https://bun.sh) installed. 53 | 54 | ```bash 55 | bun install 56 | bun run build 57 | ``` 58 | 59 | This will produce the `uwu-cli` binary in the `dist/` build output directory. 60 | 61 | ### 3. Make the binary executable and move it into your PATH 62 | 63 | ```bash 64 | chmod +x dist/uwu-cli 65 | mv dist/uwu-cli /usr/local/bin/uwu-cli 66 | ``` 67 | 68 | ### 4. Configuration 69 | 70 | `uwu` is configured through a single `config.json` file. The first time you run `uwu`, it will automatically create a default configuration file to get you started. 71 | 72 | #### Configuration File Location 73 | 74 | The `config.json` file is located in a standard, platform-specific directory: 75 | 76 | - **Linux:** `~/.config/uwu/config.json` 77 | - **macOS:** `~/Library/Preferences/uwu/config.json` 78 | - **Windows:** `%APPDATA%\\uwu\\config.json` (e.g., `C:\\Users\\\\AppData\\Roaming\\uwu\\config.json`) 79 | 80 | #### Provider Types 81 | 82 | You can configure `uwu` to use different AI providers by setting the `type` field in your `config.json`. The supported types are `"OpenAI"`, `"Custom"`, `"Claude"`, `"Gemini"`, and `"GitHub"`. 83 | 84 | Below are examples for each provider type. 85 | 86 | --- 87 | 88 | ##### **1. OpenAI (`type: "OpenAI"`)** 89 | 90 | This is the default configuration. 91 | 92 | ```json 93 | { 94 | "type": "OpenAI", 95 | "apiKey": "sk-your_openai_api_key", 96 | "model": "gpt-4.1" 97 | } 98 | ``` 99 | 100 | - `apiKey`: Your OpenAI API key. If this is empty, `uwu` will use the `OPENAI_API_KEY` environment variable. 101 | 102 | --- 103 | 104 | ##### **2. Claude (`type: "Claude"`)** 105 | 106 | Uses the native Anthropic API. 107 | 108 | ```json 109 | { 110 | "type": "Claude", 111 | "apiKey": "your-anthropic-api-key", 112 | "model": "claude-3-opus-20240229" 113 | } 114 | ``` 115 | 116 | - `apiKey`: Your Anthropic API key. 117 | 118 | --- 119 | 120 | ##### **3. Gemini (`type: "Gemini"`)** 121 | 122 | Uses the native Google Gemini API. 123 | 124 | ```json 125 | { 126 | "type": "Gemini", 127 | "apiKey": "your-google-api-key", 128 | "model": "gemini-pro" 129 | } 130 | ``` 131 | 132 | - `apiKey`: Your Google AI Studio API key. 133 | 134 | --- 135 | 136 | ##### **4. GitHub (`type: "GitHub"`)** 137 | Uses multiple free to use GitHub models. 138 | ```json 139 | { 140 | "type": "GitHub", 141 | "apiKey": "your-github-token", 142 | "model": "openai/gpt-4.1-nano" 143 | } 144 | ``` 145 | 146 | - `apiKey`: Your GitHub token. 147 | 148 | --- 149 | 150 | ##### **5. Custom / Local Models (`type: "Custom"`)** 151 | 152 | This type is for any other OpenAI-compatible API endpoint, such as Ollama, LM Studio, or a third-party proxy service. 153 | 154 | ```json 155 | { 156 | "type": "Custom", 157 | "model": "llama3", 158 | "baseURL": "http://localhost:11434/v1", 159 | "apiKey": "ollama" 160 | } 161 | ``` 162 | 163 | - `model`: The name of the model you want to use (e.g., `"llama3"`). 164 | - `baseURL`: The API endpoint for the service. 165 | - `apiKey`: An API key, if required by the service. For local models like Ollama, this can often be a non-empty placeholder like `"ollama"`. 166 | 167 | --- 168 | 169 | #### Context Configuration (Optional) 170 | 171 | `uwu` can include recent command history from your shell to provide better context for command generation. This feature is disabled by default but can be enabled. When enabled, `uwu` includes the raw last N lines from your shell history (e.g., bash, zsh, fish), preserving any extra metadata your shell records: 172 | 173 | ```json 174 | { 175 | "type": "OpenAI", 176 | "apiKey": "sk-your_api_key", 177 | "model": "gpt-4.1", 178 | "context": { 179 | "enabled": true, 180 | "maxHistoryCommands": 10 181 | } 182 | } 183 | ``` 184 | 185 | - `enabled`: Whether to include command history context (default: `false`) 186 | - `maxHistoryCommands`: Number of recent commands to include (default: `10`) 187 | When enabled, `uwu` automatically detects and parses history from bash, zsh, and fish shells. 188 | 189 | ##### Notes on history scanning performance 190 | 191 | - **Chunk size unit**: When scanning shell history files, `uwu` reads from the end of the file in fixed-size chunks of 64 KiB. This is not currently configurable but can be made if desired. 192 | 193 | ##### Windows notes 194 | 195 | - **History detection**: On Windows, `uwu` searches for PowerShell PSReadLine history at: 196 | - `%APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt` (Windows PowerShell 5.x) 197 | - `%APPDATA%\Microsoft\PowerShell\PSReadLine\ConsoleHost_history.txt` (PowerShell 7+) 198 | If not found, it falls back to Unix-like history files that may exist when using Git Bash/MSYS/Cygwin (e.g., `.bash_history`, `.zsh_history`). 199 | - **Directory listing**: On Windows, directory listing uses `dir /b`; on Linux/macOS it uses `ls`. 200 | 201 | #### Clipboard Integration 202 | 203 | `uwu` can automatically copy generated commands to your system clipboard: 204 | 205 | ```json 206 | { 207 | "type": "OpenAI", 208 | "apiKey": "sk-your_api_key", 209 | "model": "gpt-4.1", 210 | "clipboard": true 211 | } 212 | ``` 213 | 214 | - `clipboard`: Whether to automatically copy generated commands to clipboard (default: `false`) 215 | 216 | When enabled, every command generated by `uwu` is automatically copied to your system clipboard, making it easy to paste commands elsewhere. The clipboard integration works cross-platform: 217 | - **macOS**: Uses `pbcopy` 218 | - **Windows**: Uses `clip` 219 | - **Linux**: Uses `xclip` or `xsel` (falls back to `xsel` if `xclip` is not available) 220 | 221 | **Note**: On Linux, you'll need either `xclip` or `xsel` installed for clipboard functionality to work. 222 | 223 | ### 5. Configure the `uwu` helper function 224 | 225 | This function lets you type `uwu ` and get an editable command preloaded in your shell. 226 | 227 | #### zsh 228 | 229 | ```zsh 230 | # ~/.zshrc 231 | 232 | uwu() { 233 | local cmd 234 | cmd="$(uwu-cli "$@")" || return 235 | vared -p "" -c cmd 236 | print -s -- "$cmd" # add to history 237 | eval "$cmd" 238 | } 239 | ``` 240 | 241 | After editing `~/.zshrc`, reload it: 242 | 243 | ```bash 244 | source ~/.zshrc 245 | ``` 246 | 247 | #### bash 248 | ```bash 249 | uwu() { 250 | local cmd 251 | cmd="$(uwu-cli "$@")" || return 252 | # requires interactive shell and Bash 4+ 253 | read -e -i "$cmd" -p "" cmd || return 254 | builtin history -s -- "$cmd" 255 | eval -- "$cmd" 256 | } 257 | ``` 258 | 259 | #### Powershell / Conhost / Windows Terminal 260 | 261 | Note: This only applies to Windows with Powershell installed 262 | 263 | To your Powershell profile, add this snippet 264 | 265 | ```Powershell 266 | function uwu { 267 | param( 268 | [Parameter(ValueFromRemainingArguments=$true)] 269 | $args 270 | ) 271 | $Source = ' 272 | using System; 273 | using System.Runtime.InteropServices; 274 | 275 | public class ConsoleInjector { 276 | [StructLayout(LayoutKind.Sequential)] 277 | public struct KEY_EVENT_RECORD { 278 | public bool bKeyDown; 279 | public ushort wRepeatCount; 280 | public ushort wVirtualKeyCode; 281 | public ushort wVirtualScanCode; 282 | public char UnicodeChar; 283 | public uint dwControlKeyState; 284 | } 285 | 286 | [StructLayout(LayoutKind.Sequential)] 287 | public struct INPUT_RECORD { 288 | public ushort EventType; 289 | public KEY_EVENT_RECORD KeyEvent; 290 | } 291 | 292 | [DllImport("kernel32.dll", SetLastError = true)] 293 | public static extern IntPtr GetStdHandle(int nStdHandle); 294 | 295 | [DllImport("kernel32.dll", SetLastError = true)] 296 | public static extern bool WriteConsoleInput( 297 | IntPtr hConsoleInput, 298 | INPUT_RECORD[] lpBuffer, 299 | int nLength, 300 | out int lpNumberOfEventsWritten 301 | ); 302 | 303 | const int STD_INPUT_HANDLE = -10; 304 | const ushort KEY_EVENT = 0x0001; 305 | 306 | public static void SendCommand(string text) { 307 | IntPtr hIn = GetStdHandle(STD_INPUT_HANDLE); 308 | var records = new INPUT_RECORD[text.Length]; 309 | 310 | int i = 0; 311 | for (; i < text.Length; i++) { 312 | records[i].EventType = KEY_EVENT; 313 | records[i].KeyEvent.bKeyDown = true; 314 | records[i].KeyEvent.wRepeatCount = 1; 315 | records[i].KeyEvent.UnicodeChar = text[i]; 316 | } 317 | 318 | int written; 319 | WriteConsoleInput(hIn, records, i, out written); 320 | } 321 | }'; 322 | $cmd = uwu-cli @args; 323 | Add-Type -TypeDefinition $Source; 324 | [ConsoleInjector]::SendCommand($cmd) 325 | } 326 | ``` 327 | 328 | This will work for Powershell terminals. To add this functionality to Conhost / Terminal, save this as `uwu.bat` and let it be accessible in ```PATH``` (you must do the Powershell step as well). For example, 329 | 330 | ```Batch 331 | :: assumes that ECHO ON and CHCP 437 is user preference 332 | @ECHO OFF 333 | CHCP 437 >NUL 334 | POWERSHELL uwu %* 335 | @ECHO ON 336 | ``` 337 | 338 | ## Usage 339 | 340 | Once installed and configured: 341 | 342 | ```bash 343 | uwu generate a new ssh key called uwu-key and add it to the ssh agent 344 | ``` 345 | 346 | You'll see the generated command in your shell's input line. Press **Enter** to run it, or edit it first. Executed commands will show up in your shell's history just like any other command. 347 | 348 | ## License 349 | 350 | [MIT](LICENSE) 351 | 352 | ## Contributing 353 | 354 | Contributions are welcome! Please feel free to submit a pull request. 355 | -------------------------------------------------------------------------------- /assets/uwu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/context-labs/uwu/fe1171ba7a8d8ec0b7964d294674847451fcd3f9/assets/uwu.gif -------------------------------------------------------------------------------- /assets/uwu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/context-labs/uwu/fe1171ba7a8d8ec0b7964d294674847451fcd3f9/assets/uwu.jpg -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "uwu", 6 | "dependencies": { 7 | "@anthropic-ai/sdk": "^0.22.0", 8 | "@azure-rest/ai-inference": "^1.0.0-beta.6", 9 | "@azure/core-auth": "^1.10.0", 10 | "@google/generative-ai": "^0.16.0", 11 | "env-paths": "^3.0.0", 12 | "openai": "^5.12.2", 13 | "zod": "3.25.76", 14 | }, 15 | "devDependencies": { 16 | "@types/bun": "latest", 17 | }, 18 | "peerDependencies": { 19 | "typescript": "^5", 20 | }, 21 | }, 22 | }, 23 | "packages": { 24 | "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.22.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7", "web-streams-polyfill": "^3.2.1" } }, "sha512-dv4BCC6FZJw3w66WNLsHlUFjhu19fS1L/5jMPApwhZLa/Oy1j0A2i3RypmDtHEPp4Wwg3aZkSHksp7VzYWjzmw=="], 25 | 26 | "@azure-rest/ai-inference": ["@azure-rest/ai-inference@1.0.0-beta.6", "", { "dependencies": { "@azure-rest/core-client": "^2.1.0", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.9.0", "@azure/core-lro": "^2.7.2", "@azure/core-rest-pipeline": "^1.18.2", "@azure/core-tracing": "^1.2.0", "@azure/logger": "^1.1.4", "tslib": "^2.8.1" } }, "sha512-j5FrJDTHu2P2+zwFVe5j2edasOIhqkFj+VkDjbhGkQuOoIAByF0egRkgs0G1k03HyJ7bOOT9BkRF7MIgr/afhw=="], 27 | 28 | "@azure-rest/core-client": ["@azure-rest/core-client@2.5.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-rest-pipeline": "^1.5.0", "@azure/core-tracing": "^1.0.1", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-KMVIPxG6ygcQ1M2hKHahF7eddKejYsWTjoLIfTWiqnaj42dBkYzj4+S8rK9xxmlOaEHKZHcMrRbm0NfN4kgwHw=="], 29 | 30 | "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], 31 | 32 | "@azure/core-auth": ["@azure/core-auth@1.10.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.11.0", "tslib": "^2.6.2" } }, "sha512-88Djs5vBvGbHQHf5ZZcaoNHo6Y8BKZkt3cw2iuJIQzLEgH4Ox6Tm4hjFhbqOxyYsgIG/eJbFEHpxRIfEEWv5Ow=="], 33 | 34 | "@azure/core-lro": ["@azure/core-lro@2.7.2", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-util": "^1.2.0", "@azure/logger": "^1.0.0", "tslib": "^2.6.2" } }, "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw=="], 35 | 36 | "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.8.0", "@azure/core-tracing": "^1.0.1", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-OKHmb3/Kpm06HypvB3g6Q3zJuvyXcpxDpCS1PnU8OV6AJgSFaee/covXBcPbWc6XDDxtEPlbi3EMQ6nUiPaQtw=="], 37 | 38 | "@azure/core-tracing": ["@azure/core-tracing@1.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+XvmZLLWPe67WXNZo9Oc9CrPj/Tm8QnHR92fFAFdnbzwNdCH1h+7UdpaQgRSBsMY+oW1kHXNUZQLdZ1gHX3ROw=="], 39 | 40 | "@azure/core-util": ["@azure/core-util@1.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-o0psW8QWQ58fq3i24Q1K2XfS/jYTxr7O1HRcyUE9bV9NttLU+kYOH82Ixj8DGlMTOWgxm1Sss2QAfKK5UkSPxw=="], 41 | 42 | "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], 43 | 44 | "@google/generative-ai": ["@google/generative-ai@0.16.1", "", {}, "sha512-t4x4g0z/HT2BdBNfK2ua2xA/Az+SDFng4PxWjgiys/qxbh2YcrCI2rZg9/6eBkd4Iz41yjpCCDOWxsMryLJ7TA=="], 45 | 46 | "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], 47 | 48 | "@types/node": ["@types/node@18.19.122", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-yzegtT82dwTNEe/9y+CM8cgb42WrUfMMCg2QqSddzO1J6uPmBD7qKCZ7dOHZP2Yrpm/kb0eqdNMn2MUyEiqBmA=="], 49 | 50 | "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], 51 | 52 | "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], 53 | 54 | "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.0", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-sOx1PKSuFwnIl7z4RN0Ls7N9AQawmR9r66eI5rFCzLDIs8HTIYrIpH9QjYWoX0lkgGrkLxXhi4QnK7MizPRrIg=="], 55 | 56 | "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], 57 | 58 | "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], 59 | 60 | "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], 61 | 62 | "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], 63 | 64 | "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], 65 | 66 | "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 67 | 68 | "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], 69 | 70 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 71 | 72 | "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], 73 | 74 | "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], 75 | 76 | "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 77 | 78 | "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], 79 | 80 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 81 | 82 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 83 | 84 | "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 85 | 86 | "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], 87 | 88 | "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 89 | 90 | "form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="], 91 | 92 | "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], 93 | 94 | "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], 95 | 96 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 97 | 98 | "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 99 | 100 | "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 101 | 102 | "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 103 | 104 | "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 105 | 106 | "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], 107 | 108 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 109 | 110 | "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], 111 | 112 | "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], 113 | 114 | "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], 115 | 116 | "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 117 | 118 | "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 119 | 120 | "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 121 | 122 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 123 | 124 | "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 125 | 126 | "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], 127 | 128 | "openai": ["openai@5.12.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ=="], 129 | 130 | "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], 131 | 132 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 133 | 134 | "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 135 | 136 | "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], 137 | 138 | "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 139 | 140 | "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], 141 | 142 | "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], 143 | 144 | "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 145 | 146 | "@types/node-fetch/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], 147 | 148 | "bun-types/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], 149 | 150 | "formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], 151 | 152 | "@types/node-fetch/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 153 | 154 | "bun-types/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /context.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import os from "os"; 4 | 5 | export interface ContextConfig { 6 | enabled?: boolean; 7 | maxHistoryCommands?: number; 8 | } 9 | 10 | export const DEFAULT_CONTEXT_CONFIG: ContextConfig = { 11 | enabled: false, 12 | maxHistoryCommands: 10, 13 | }; 14 | 15 | // Size of each backwards-read chunk when scanning shell history files. 16 | // Unit: bytes. 64 KiB balances I/O efficiency with memory usage. 17 | const HISTORY_READ_CHUNK_SIZE_BYTES = 64 * 1024; 18 | 19 | // Shell history parsing functions 20 | function getHistoryFilePath(): string | null { 21 | const shell = process.env.SHELL || ""; 22 | const home = os.homedir(); 23 | const isWindows = process.platform === "win32"; 24 | 25 | // Windows: Prefer PowerShell PSReadLine history if available 26 | if (isWindows) { 27 | const appData = 28 | process.env.APPDATA || path.join(home, "AppData", "Roaming"); 29 | const psHistoryCandidates = [ 30 | // Windows PowerShell 5.x 31 | path.join( 32 | appData, 33 | "Microsoft", 34 | "Windows", 35 | "PowerShell", 36 | "PSReadLine", 37 | "ConsoleHost_history.txt" 38 | ), 39 | // PowerShell 7+ 40 | path.join( 41 | appData, 42 | "Microsoft", 43 | "PowerShell", 44 | "PSReadLine", 45 | "ConsoleHost_history.txt" 46 | ), 47 | ]; 48 | for (const psPath of psHistoryCandidates) { 49 | if (fs.existsSync(psPath)) return psPath; 50 | } 51 | // If nothing found on Windows, fall through to shared fallbacks that may exist under Git Bash, MSYS, or Cygwin 52 | } 53 | 54 | if (shell.includes("zsh")) { 55 | return process.env.HISTFILE || path.join(home, ".zsh_history"); 56 | } else if (shell.includes("bash")) { 57 | return process.env.HISTFILE || path.join(home, ".bash_history"); 58 | } else if (shell.includes("fish")) { 59 | const macFish = path.join( 60 | home, 61 | "Library", 62 | "Application Support", 63 | "fish", 64 | "fish_history" 65 | ); 66 | const linuxFish = path.join( 67 | home, 68 | ".local", 69 | "share", 70 | "fish", 71 | "fish_history" 72 | ); 73 | if (fs.existsSync(macFish)) return macFish; 74 | return linuxFish; 75 | } 76 | 77 | // Try common paths as fallback 78 | const commonPaths = [ 79 | path.join(home, ".zsh_history"), 80 | path.join(home, ".bash_history"), 81 | path.join(home, ".local", "share", "fish", "fish_history"), 82 | path.join(home, "Library", "Application Support", "fish", "fish_history"), 83 | ]; 84 | 85 | for (const histPath of commonPaths) { 86 | if (fs.existsSync(histPath)) { 87 | return histPath; 88 | } 89 | } 90 | 91 | return null; 92 | } 93 | 94 | /** 95 | * Efficiently read the last N non-empty lines of a file without loading the whole file. 96 | * 97 | * Reads the file from the end in fixed-size chunks to avoid loading it entirely. 98 | * Chunk size is defined by `HISTORY_READ_CHUNK_SIZE_BYTES` (bytes). 99 | */ 100 | function readLastLines({ 101 | filePath, 102 | maxLines, 103 | }: { 104 | filePath: string; 105 | maxLines: number; 106 | }): string[] { 107 | let fd: number | null = null; 108 | try { 109 | fd = fs.openSync(filePath, "r"); 110 | const { size } = fs.fstatSync(fd); 111 | if (size === 0) return []; 112 | 113 | let position = size; 114 | let accumulator = ""; 115 | const buffer = Buffer.allocUnsafe( 116 | Math.min(HISTORY_READ_CHUNK_SIZE_BYTES, size) 117 | ); 118 | 119 | const hasEnoughLines = () => 120 | (accumulator.match(/\n/g)?.length || 0) >= maxLines + 1; 121 | 122 | while (position > 0 && !hasEnoughLines()) { 123 | const readLength = Math.min(buffer.length, position); 124 | position -= readLength; 125 | fs.readSync(fd, buffer, 0, readLength, position); 126 | accumulator = buffer.toString("utf8", 0, readLength) + accumulator; 127 | } 128 | 129 | const allLines = accumulator.split("\n"); 130 | const nonEmpty = allLines.filter((l) => l.trim()); 131 | return nonEmpty.slice(-maxLines); 132 | } catch { 133 | return []; 134 | } finally { 135 | if (fd !== null) { 136 | try { 137 | fs.closeSync(fd); 138 | } catch {} 139 | } 140 | } 141 | } 142 | 143 | function getRecentCommands(maxCommands: number): string[] { 144 | const historyPath = getHistoryFilePath(); 145 | if (!historyPath) { 146 | return []; 147 | } 148 | 149 | try { 150 | // Include full raw lines from the history file for richer context 151 | return readLastLines({ filePath: historyPath, maxLines: maxCommands }); 152 | } catch { 153 | return []; 154 | } 155 | } 156 | 157 | export function buildContextHistory(contextConfig: ContextConfig): string { 158 | if (!contextConfig.enabled) { 159 | return ""; 160 | } 161 | 162 | let historyContext = ""; 163 | 164 | // Get recent commands 165 | const recentCommands = getRecentCommands( 166 | contextConfig.maxHistoryCommands || 167 | DEFAULT_CONTEXT_CONFIG.maxHistoryCommands! 168 | ); 169 | if (recentCommands.length > 0) { 170 | historyContext += "\n--- RECENT COMMANDS ---\n"; 171 | historyContext += "Recent shell commands (most recent last):\n"; 172 | recentCommands.forEach((cmd, idx) => { 173 | historyContext += `${idx + 1}. ${cmd}\n`; 174 | }); 175 | historyContext += "--- END COMMAND HISTORY ---\n"; 176 | } 177 | 178 | return historyContext; 179 | } 180 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from 'openai'; 2 | import Anthropic from '@anthropic-ai/sdk'; 3 | import { GoogleGenerativeAI } from '@google/generative-ai'; 4 | import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'; 5 | import { AzureKeyCredential } from '@azure/core-auth'; 6 | import { $ } from "bun"; 7 | import os from "os"; 8 | import fs from "fs"; 9 | import path from "path"; 10 | import envPaths from "env-paths"; 11 | 12 | import { DEFAULT_CONTEXT_CONFIG, buildContextHistory } from "./context"; 13 | import type { ContextConfig } from "./context"; 14 | 15 | type ProviderType = "OpenAI" | "Custom" | "Claude" | "Gemini" | "GitHub"; 16 | 17 | interface Config { 18 | type: ProviderType; 19 | apiKey?: string; 20 | model: string; 21 | baseURL?: string; 22 | context?: ContextConfig; 23 | clipboard?: boolean; 24 | } 25 | 26 | const DEFAULT_CONFIG: Config = { 27 | type: "OpenAI", 28 | model: "gpt-4.1", 29 | context: DEFAULT_CONTEXT_CONFIG, 30 | clipboard: false, 31 | }; 32 | 33 | function getConfig(): Config { 34 | const paths = envPaths("uwu", { suffix: "" }); 35 | const configPath = path.join(paths.config, "config.json"); 36 | 37 | if (!fs.existsSync(configPath)) { 38 | try { 39 | // If the config file doesn't exist, create it with defaults. 40 | fs.mkdirSync(paths.config, { recursive: true }); 41 | const defaultConfigToFile = { 42 | ...DEFAULT_CONFIG, 43 | apiKey: "", 44 | baseURL: null, 45 | clipboard: false, 46 | }; 47 | fs.writeFileSync( 48 | configPath, 49 | JSON.stringify(defaultConfigToFile, null, 2) 50 | ); 51 | 52 | // For this first run, use the environment variable for the API key. 53 | // The newly created file has an empty key, so subsequent runs will also fall back to the env var until the user edits the file. 54 | return { 55 | ...DEFAULT_CONFIG, 56 | apiKey: process.env.OPENAI_API_KEY, 57 | }; 58 | } catch (error) { 59 | console.error("Error creating the configuration file at:", configPath); 60 | console.error("Please check your permissions for the directory."); 61 | process.exit(1); 62 | } 63 | } 64 | 65 | try { 66 | const rawConfig = fs.readFileSync(configPath, "utf-8"); 67 | const userConfig = JSON.parse(rawConfig); 68 | 69 | // Merge user config with defaults, and also check env for API key as a fallback. 70 | const mergedConfig = { 71 | ...DEFAULT_CONFIG, 72 | ...userConfig, 73 | apiKey: userConfig.apiKey || process.env.OPENAI_API_KEY, 74 | }; 75 | 76 | // Ensure context config has all defaults filled in 77 | if (mergedConfig.context) { 78 | mergedConfig.context = { 79 | ...DEFAULT_CONTEXT_CONFIG, 80 | ...mergedConfig.context, 81 | }; 82 | } else { 83 | mergedConfig.context = DEFAULT_CONTEXT_CONFIG; 84 | } 85 | 86 | // Ensure clipboard config has default 87 | if (mergedConfig.clipboard === undefined) { 88 | mergedConfig.clipboard = false; 89 | } 90 | 91 | return mergedConfig; 92 | } catch (error) { 93 | console.error( 94 | "Error reading or parsing the configuration file at:", 95 | configPath 96 | ); 97 | console.error("Please ensure it is a valid JSON file."); 98 | process.exit(1); 99 | } 100 | } 101 | 102 | async function copyToClipboard(text: string): Promise { 103 | const { execSync } = await import('child_process'); 104 | 105 | try { 106 | if (process.platform === "darwin") { 107 | execSync("pbcopy", { input: text }); 108 | } else if (process.platform === "win32") { 109 | execSync("clip", { input: text }); 110 | } else { 111 | // Linux - try xclip first, then xsel 112 | try { 113 | execSync("xclip -selection clipboard", { input: text }); 114 | } catch { 115 | execSync("xsel --clipboard --input", { input: text }); 116 | } 117 | } 118 | } catch (error) { 119 | throw new Error(`Clipboard operation failed: ${error}`); 120 | } 121 | } 122 | 123 | const config = getConfig(); 124 | 125 | // The rest of the arguments are the command description 126 | const commandDescription = process.argv.slice(2).join(" ").trim(); 127 | 128 | if (!commandDescription) { 129 | console.error("Error: No command description provided."); 130 | console.error("Usage: uwu "); 131 | process.exit(1); 132 | } 133 | 134 | function sanitizeResponse(content: string): string { 135 | if (!content) return ""; 136 | 137 | content = content.replace( 138 | /<\s*think\b[^>]*>[\s\S]*?<\s*\/\s*think\s*>/gi, 139 | "" 140 | ); 141 | 142 | let lastCodeBlock: string | null = null; 143 | const codeBlockRegex = /```(?:[^\n]*)\n([\s\S]*?)```/g; 144 | let m; 145 | while ((m = codeBlockRegex.exec(content)) !== null) { 146 | lastCodeBlock = m[1] || ''; 147 | } 148 | if (lastCodeBlock) { 149 | content = lastCodeBlock; 150 | } else { 151 | content = content.replace(/`/g, ""); 152 | } 153 | 154 | const lines = content 155 | .split(/\r?\n/) 156 | .map((l) => l.trim()) 157 | .filter(Boolean); 158 | if (lines.length === 0) return ""; 159 | 160 | for (let i = lines.length - 1; i >= 0; i--) { 161 | const line = lines[i]!; 162 | 163 | const looksLikeSentence = 164 | /^[A-Z][\s\S]*[.?!]$/.test(line) || 165 | /\b(user|want|should|shouldn't|think|explain|error|note)\b/i.test(line); 166 | if (!looksLikeSentence && line.length <= 2000) { 167 | return line.trim(); 168 | } 169 | } 170 | 171 | return lines.at(-1)?.trim() || ''; 172 | } 173 | 174 | async function generateCommand( 175 | config: Config, 176 | commandDescription: string 177 | ): Promise { 178 | const envContext = ` 179 | Operating System: ${os.type()} ${os.release()} (${os.platform()} - ${os.arch()}) 180 | Node.js Version: ${process.version} 181 | Shell: ${process.env.SHELL || "unknown"} 182 | Current Working Directory: ${process.cwd()} 183 | Home Directory: ${os.homedir()} 184 | CPU Info: ${os.cpus()[0]?.model} (${os.cpus().length} cores) 185 | Total Memory: ${(os.totalmem() / 1024 / 1024).toFixed(0)} MB 186 | Free Memory: ${(os.freemem() / 1024 / 1024).toFixed(0)} MB 187 | `; 188 | 189 | // Get directory listing (`ls` on Unix, `dir` on Windows) 190 | let lsResult = ""; 191 | let lsCommand = ""; 192 | try { 193 | if (process.platform === "win32") { 194 | // Use PowerShell-compatible dir for a simple listing 195 | lsCommand = "dir /b"; 196 | lsResult = await $`cmd /c ${lsCommand}`.text(); 197 | } else { 198 | lsCommand = "ls"; 199 | lsResult = await $`${lsCommand}`.text(); 200 | } 201 | } catch (error) { 202 | lsResult = "Unable to get directory listing"; 203 | } 204 | 205 | // Build command history context if enabled 206 | const contextConfig = config.context || DEFAULT_CONTEXT_CONFIG; 207 | const historyContext = buildContextHistory(contextConfig); 208 | 209 | // System prompt 210 | const systemPrompt = ` 211 | You live in a developer's CLI, helping them convert natural language into CLI commands. 212 | Based on the description of the command given, generate the command. Output only the command and nothing else. 213 | Make sure to escape characters when appropriate. The result of \`${lsCommand}\` is given with the command. 214 | This may be helpful depending on the description given. Do not include any other text in your response, except for the command. 215 | Do not wrap the command in quotes. 216 | 217 | --- ENVIRONMENT CONTEXT --- 218 | ${envContext} 219 | --- END ENVIRONMENT CONTEXT --- 220 | 221 | Result of \`${lsCommand}\` in working directory: 222 | ${lsResult} 223 | ${historyContext}`; 224 | 225 | if (!config.apiKey) { 226 | console.error("Error: API key not found."); 227 | console.error( 228 | "Please provide an API key in your config.json file or by setting the OPENAI_API_KEY environment variable." 229 | ); 230 | process.exit(1); 231 | } 232 | 233 | switch (config.type) { 234 | case "OpenAI": 235 | case "Custom": { 236 | const openai = new OpenAI({ 237 | apiKey: config.apiKey, 238 | baseURL: config.baseURL, 239 | }); 240 | const response = await openai.chat.completions.create({ 241 | model: config.model, 242 | messages: [ 243 | { role: "system", content: systemPrompt }, 244 | { 245 | role: "user", 246 | content: `Command description: ${commandDescription}`, 247 | }, 248 | ], 249 | }); 250 | const raw = response?.choices?.[0]?.message?.content ?? ""; 251 | return sanitizeResponse(String(raw)); 252 | } 253 | 254 | case "Claude": { 255 | const anthropic = new Anthropic({ apiKey: config.apiKey }); 256 | const response = await anthropic.messages.create({ 257 | model: config.model, 258 | system: systemPrompt, 259 | max_tokens: 1024, 260 | messages: [ 261 | { 262 | role: "user", 263 | content: `Command description: ${commandDescription}`, 264 | }, 265 | ], 266 | }); 267 | // @ts-ignore 268 | const raw = response.content?.[0].text ?? response?.text ?? ''; 269 | return sanitizeResponse(String(raw)); 270 | } 271 | 272 | case "Gemini": { 273 | const genAI = new GoogleGenerativeAI(config.apiKey); 274 | const model = genAI.getGenerativeModel({ model: config.model }); 275 | const prompt = `${systemPrompt}\n\nCommand description: ${commandDescription}`; 276 | const result = await model.generateContent(prompt); 277 | const response = await result.response; 278 | const raw = await response.text(); 279 | return sanitizeResponse(String(raw)); 280 | } 281 | 282 | case "GitHub": { 283 | const endpoint = config.baseURL ? config.baseURL : "https://models.github.ai/inference"; 284 | const model = config.model ? config.model : "openai/gpt-4.1-nano"; 285 | const github = ModelClient( 286 | endpoint, 287 | new AzureKeyCredential(config.apiKey) 288 | ); 289 | 290 | const response = await github.path("/chat/completions").post({ 291 | body: { 292 | messages: [ 293 | { role: "system", content: systemPrompt }, 294 | { role: "user", content: `Command description: ${commandDescription}` }, 295 | ], 296 | temperature: 1.0, 297 | top_p: 1.0, 298 | model: model, 299 | }, 300 | }); 301 | 302 | if (isUnexpected(response)) { 303 | throw response.body.error; 304 | } 305 | 306 | const content = response.body.choices?.[0]?.message?.content; 307 | return content?.trim() || ""; 308 | } 309 | 310 | default: 311 | console.error( 312 | `Error: Unknown provider type "${config.type}" in config.json.` 313 | ); 314 | process.exit(1); 315 | } 316 | } 317 | 318 | // --- Main Execution --- 319 | try { 320 | const command = await generateCommand(config, commandDescription); 321 | 322 | // Copy to clipboard if enabled 323 | if (config.clipboard) { 324 | try { 325 | await copyToClipboard(command); 326 | } catch (clipboardError: any) { 327 | console.error("Warning: Failed to copy to clipboard:", clipboardError.message); 328 | } 329 | } 330 | 331 | console.log(command); 332 | } catch (error: any) { 333 | console.error("Error generating command:", error.message); 334 | process.exit(1); 335 | } 336 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uwu", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "bun run index.ts", 8 | "build": "bun build ./index.ts --compile --outfile dist/uwu-cli" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5" 15 | }, 16 | "dependencies": { 17 | "@anthropic-ai/sdk": "^0.22.0", 18 | "@azure-rest/ai-inference": "^1.0.0-beta.6", 19 | "@azure/core-auth": "^1.10.0", 20 | "@google/generative-ai": "^0.16.0", 21 | "env-paths": "^3.0.0", 22 | "openai": "^5.12.2", 23 | "zod": "3.25.76" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample_configs/claude.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Claude", 3 | "apiKey": "your-anthropic-api-key", 4 | "model": "claude-3-opus-20240229" 5 | } 6 | -------------------------------------------------------------------------------- /sample_configs/clipboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "OpenAI", 3 | "apiKey": "", 4 | "model": "gpt-4.1", 5 | "context": { 6 | "enabled": false, 7 | "maxHistoryCommands": 10 8 | }, 9 | "clipboard": true 10 | } -------------------------------------------------------------------------------- /sample_configs/context.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "OpenAI", 3 | "apiKey": "", 4 | "model": "gpt-4", 5 | "context": { 6 | "enabled": true, 7 | "maxHistoryCommands": 10 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample_configs/gemini.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Gemini", 3 | "apiKey": "your-google-api-key", 4 | "model": "gemini-pro" 5 | } 6 | -------------------------------------------------------------------------------- /sample_configs/github.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "GitHub", 3 | "apiKey": "your-github-token", 4 | "model": "model-available-at-https://github.com/marketplace/models/", 5 | "baseURL": "https://models.github.ai/inference" 6 | } 7 | -------------------------------------------------------------------------------- /sample_configs/ollama.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Custom", 3 | "model": "llama3", 4 | "baseURL": "http://localhost:11434/v1", 5 | "apiKey": "ollama" 6 | } 7 | -------------------------------------------------------------------------------- /sample_configs/openai.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "OpenAI", 3 | "apiKey": "sk-your_openai_api_key", 4 | "model": "gpt-4.1" 5 | } 6 | -------------------------------------------------------------------------------- /scripts/test-uwu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "=== uwu Context Feature Test ===" 4 | echo "" 5 | 6 | # Check if API key is set 7 | if [ -z "$OPENAI_API_KEY" ]; then 8 | echo "⚠️ OPENAI_API_KEY is not set in the environment" 9 | echo "Please run: export OPENAI_API_KEY='your-api-key-here'" 10 | echo "" 11 | echo "Or add it to your config file at:" 12 | if [[ "$OSTYPE" == darwin* ]]; then 13 | echo " ~/Library/Preferences/uwu/config.json" 14 | else 15 | echo " ~/.config/uwu/config.json" 16 | fi 17 | exit 1 18 | fi 19 | 20 | echo "✅ OPENAI_API_KEY is set" 21 | echo "" 22 | 23 | # Test 1: Basic command without context 24 | echo "Test 1: Basic command (context disabled by default)" 25 | 26 | # Ensure binary exists 27 | if [ ! -x "./dist/uwu-cli" ]; then 28 | echo "Building uwu-cli..." 29 | bun run build || { echo "Build failed"; exit 1; } 30 | fi 31 | 32 | echo "Running: ./dist/uwu-cli 'list files in current directory'" 33 | ./dist/uwu-cli "list files in current directory" 34 | echo "" 35 | 36 | # Create a config with context enabled 37 | CONFIG_DIR="" 38 | if [[ "$OSTYPE" == darwin* ]]; then 39 | CONFIG_DIR="$HOME/Library/Preferences/uwu" 40 | else 41 | CONFIG_DIR="$HOME/.config/uwu" 42 | fi 43 | 44 | echo "Test 2: Creating config with context enabled" 45 | mkdir -p "$CONFIG_DIR" 46 | cat > "$CONFIG_DIR/config.json" << EOF 47 | { 48 | "type": "OpenAI", 49 | "apiKey": "", 50 | "model": "gpt-4", 51 | "context": { 52 | "enabled": true, 53 | "maxHistoryCommands": 10 54 | } 55 | } 56 | EOF 57 | 58 | echo "Config created at: $CONFIG_DIR/config.json" 59 | echo "" 60 | 61 | # Run some commands to build history 62 | echo "Building command history..." 63 | echo "$ git status" 64 | git status 65 | echo "" 66 | echo "$ ls *.md" 67 | ls *.md 68 | echo "" 69 | 70 | # Test with context 71 | echo "Test 3: Command with context enabled" 72 | echo "Running: ./dist/uwu-cli 'show me the markdown files I just listed'" 73 | ./dist/uwu-cli "show me the markdown files I just listed" 74 | 75 | echo "" 76 | echo "=== Test Complete ===" 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedIndexedAccess": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | --------------------------------------------------------------------------------