├── .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 |
4 |
5 | uwu
6 |
7 |
8 |
9 | ✨ Natural language to shell commands using AI ✨
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 | 
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 |
--------------------------------------------------------------------------------