Skip to content

Commit f2a64ab

Browse files
committed
feat: add caching, shell integration, aliases, script/recipe mode, streaming animation, and more
Performance: - Skip tool extraction for coreutils-only prompts (eliminates LLM call) - Prompt→command cache (hash-based JSONL at ~/.cache/eai/) - Auto-retry on 429 rate limit (3s wait) Accuracy: - Few-shot examples in system prompt (3 diverse examples) - Project context detection (Cargo.toml, package.json, go.mod, Dockerfile, etc.) - Post-generation validation (warns if binary not in PATH) New modes: - --script: generate full shell scripts instead of one-liners - --recipe: generate numbered step-by-step workflows - --demo: curated examples without API key UX: - Typewriter animation on first command display - Shell integration via Ctrl+E (zsh/bash/fish) — `eai init <shell>` - Shell completions via clap_complete — `eai completions <shell>` - Command bookmarks — `eai save`, `eai @alias`, `eai aliases`, `eai unsave` - Fuzzy history search with scoring - Pipe mode auto-detects content type (JSON, CSV, error output, HTML, Markdown) Made-with: Cursor
1 parent f0d890f commit f2a64ab

14 files changed

Lines changed: 1118 additions & 45 deletions

File tree

AGENTS.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ Rust CLI that converts natural language to shell commands using LLMs. Single bin
99
```
1010
src/
1111
main.rs — entry point, tokio runtime
12-
cli.rs — clap arg definitions (Cli, Commands)
13-
app.rs — orchestration: prompt → generate → confirm → execute loop
12+
cli.rs — clap arg definitions (Cli, Commands, shell completions)
13+
app.rs — orchestration: prompt → generate → confirm → execute loop, modes (script/recipe/explain/demo)
1414
config.rs — TOML config at ~/.config/eai/config.toml
1515
setup.rs — interactive onboarding wizard (eai setup) + optional Tavily setup
16-
ui.rs — all terminal rendering: banner, gradient box, action bar, spinners, tool suggestions
16+
ui.rs — all terminal rendering: banner, gradient box, action bar, spinners, typewriter animation
1717
types.rs — shared types: BackendKind, ShellKind, CommandRequest, GeneratedCommand
1818
tool_context.rs — tool detection, discovery, package registry verification, install flow
1919
tldr.rs — embedded tldr-pages lookup (7000+ commands, zstd-compressed, O(1) HashMap)
20+
cache.rs — file-based prompt→command cache at ~/.cache/eai/cache.jsonl
21+
aliases.rs — command bookmarks at ~/.config/eai/aliases.json
2022
search.rs — web search: Tavily (preferred) or DuckDuckGo (fallback)
2123
history.rs — append-only JSONL history at ~/.local/share/eai/history.jsonl
2224
llm/
23-
mod.rs — Backend trait, prompt building, response parsing, backend resolution, shell profile fallback
25+
mod.rs — Backend trait, prompt building, response parsing, OS flag correction, backend resolution
2426
openai.rs — OpenAI-compatible client (also used for Groq, OpenRouter)
2527
ollama.rs — Ollama local client
2628
claude.rs — Claude CLI wrapper
@@ -32,15 +34,20 @@ build.rs — downloads tldr-pages at build time, parses markdown, seri
3234
- All UI output goes to **stderr** (`eprintln!`). Only command execution output goes to stdout.
3335
- RGB gradients use raw ANSI escapes (`\x1b[38;2;R;G;Bm`) with `colors_enabled_stderr()` fallback.
3436
- `GeneratedCommand` has `.command` (the shell command) and `.explanation` (optional `//` comment from LLM).
35-
- `parse_response` in `llm/mod.rs` extracts command + explanation from LLM output. Tolerant of markdown fences.
36-
- Tool extraction uses a separate LLM call. Filter: ASCII-only, `is_noise_word()` blocklist, max 5 tools.
37-
- Pipe mode: `read_stdin_if_piped()` reads up to 4K chars from stdin when not a terminal.
37+
- `parse_response` in `llm/mod.rs` extracts command + explanation from LLM output. Tolerant of markdown fences. Applies OS-specific flag corrections.
38+
- Tool extraction uses a separate LLM call. Filter: ASCII-only, `is_noise_word()` blocklist, max 5 tools. Skipped for coreutils-only prompts.
39+
- Pipe mode: `read_stdin_if_piped()` reads up to 4K chars from stdin; auto-detects content type (JSON, CSV, error output, etc.).
3840
- The `build_openai_compat()` helper in `llm/mod.rs` handles both Groq and OpenAI backends.
3941
- API keys read from env vars; `env_var()` in `llm/mod.rs` falls back to reading from shell profile.
4042
- Tool docs resolution: embedded tldr-pages (instant O(1) lookup) + `--help` output combined. Even tools not installed get tldr docs.
4143
- Tool discovery: when all extracted tools are missing, searches web + LLM suggests alternatives, verifies against package registries (brew, PyPI, npm, crates.io), offers interactive install.
4244
- Search hierarchy: Tavily (if `TAVILY_API_KEY` set) → DuckDuckGo (fallback). Configured via `search.engine` in config.
4345
- Setup hides API key input (Password prompt) and validates keys with retry on 401.
46+
- Prompt→command cache (hash-based) avoids repeated LLM calls for identical prompts.
47+
- Post-generation validation checks if the main binary exists in PATH and warns if not.
48+
- Project context detection (Cargo.toml, package.json, go.mod, etc.) enriches the LLM prompt.
49+
- 429 rate-limit auto-retry (3s wait) before surfacing the error.
50+
- Typewriter animation on first command display (TTY only).
4451

4552
## Conventions
4653

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ async-trait = "0.1"
1717
bincode = "2"
1818
chrono = { version = "0.4", features = ["serde"] }
1919
clap = { version = "4.5", features = ["derive"] }
20+
clap_complete = "4.6.2"
2021
console = "0.15"
2122
dialoguer = "0.11"
2223
dirs = "6.0"

README.md

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,57 @@ When you ask for a tool that isn't installed, eai searches the web, finds real a
131131
╰──────────────────────────────────────────────╯
132132
```
133133

134+
### Script mode — full scripts instead of one-liners
135+
136+
```bash
137+
eai --script "backup my database and compress it" > backup.sh
138+
eai --script "setup a new Node.js project with TypeScript" > setup.sh
139+
```
140+
141+
### Recipe mode — step-by-step workflows
142+
143+
```bash
144+
eai --recipe "deploy a docker container to production"
145+
# Step 1: Build the image
146+
# ❯ docker build -t myapp .
147+
# Step 2: Tag for registry
148+
# ❯ docker tag myapp registry.example.com/myapp:latest
149+
# ...
150+
```
151+
152+
### Command aliases — save & reuse
153+
154+
```bash
155+
eai save deploy "git push origin main" --desc "push to production"
156+
eai @deploy # runs the saved command directly
157+
eai aliases # list all saved aliases
158+
eai unsave deploy # remove an alias
159+
```
160+
161+
### Shell integration — Ctrl+E
162+
163+
```bash
164+
eval "$(eai init zsh)" # add to .zshrc
165+
eval "$(eai init bash)" # add to .bashrc
166+
eai init fish | source # add to config.fish
167+
# Then press Ctrl+E to translate the current line into a command
168+
```
169+
170+
### Demo mode — try without API key
171+
172+
```bash
173+
eai --demo # shows curated examples, no LLM needed
174+
```
175+
134176
### Flags
135177

136178
```bash
137179
eai --dry "..." # show command, don't run
138180
eai --explain "..." # explain a command (alias: --wtf)
181+
eai --script "..." # generate a full shell script
182+
eai --recipe "..." # generate a multi-step workflow
139183
eai --search "..." # force web search before generating
184+
eai --demo # offline demo with sample commands
140185
eai -b groq "..." # force a specific backend
141186
eai -m llama-3.3-70b-versatile "..." # force a specific model
142187
eai --no-confirm "..." # skip confirmation (yolo)
@@ -150,6 +195,11 @@ eai setup # interactive provider setup wizard
150195
eai config # open config in $EDITOR
151196
eai history # show recent commands
152197
eai history --search docker
198+
eai completions zsh # generate shell completions
199+
eai init zsh # output Ctrl+E shell integration
200+
eai save <name> <cmd> # bookmark a command
201+
eai aliases # list bookmarks
202+
eai unsave <name> # remove bookmark
153203
```
154204

155205
## How it works
@@ -234,13 +284,19 @@ The suite uses a mocked `claude` CLI and validates end-to-end flows for:
234284

235285
| Feature | eai | llm | aichat | shell-gpt |
236286
|---|---|---|---|---|
237-
| **Pipe context (stdin)** |||||
287+
| **Pipe context (stdin)** |(auto-detects JSON/CSV/error) ||||
238288
| **Explain mode** | ✓ (`--wtf`) ||| partial |
289+
| **Script/recipe generation** | ✓ (`--script`, `--recipe`) ||||
290+
| **Command caching** | ✓ (instant repeat queries) ||||
291+
| **Shell integration (Ctrl+E)** | ✓ (zsh/bash/fish) ||||
292+
| **Command bookmarks** | ✓ (`save`/`@alias`) ||||
293+
| **Project-aware** | ✓ (auto-detects Cargo/npm/Go/Docker) ||||
239294
| **Free by default** | ✓ (Gemini/Groq/Ollama) | ✗ (OpenAI) | ✗ (Needs API) | ✗ (OpenAI) |
240295
| **Auto-retry on error** | ✓ (feeds stderr back) ||||
241296
| **Web search** | ✓ (Tavily/DDG) | ✗ (plugins) | partial | ✗ (plugins) |
242297
| **Tool doc detection** | ✓ (7000+ embedded + --help) ||||
243298
| **Tool discovery + install** | ✓ (registry-verified) ||||
299+
| **Shell completions** | ✓ (zsh/bash/fish) ||||
244300
| **Setup wizard** | ✓ (30s) ||||
245301
| **Single binary** | ✓ (Rust) | Python | ✓ (Rust) | Python |
246302

src/aliases.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::collections::HashMap;
2+
use std::fs;
3+
use std::path::PathBuf;
4+
5+
use anyhow::{Context, Result};
6+
use serde::{Deserialize, Serialize};
7+
8+
#[derive(Serialize, Deserialize, Default)]
9+
struct AliasStore {
10+
aliases: HashMap<String, AliasEntry>,
11+
}
12+
13+
#[derive(Serialize, Deserialize, Clone)]
14+
pub struct AliasEntry {
15+
pub command: String,
16+
pub description: Option<String>,
17+
}
18+
19+
fn aliases_path() -> Result<PathBuf> {
20+
let dir = dirs::config_dir()
21+
.context("cannot find config dir")?
22+
.join("eai");
23+
fs::create_dir_all(&dir)?;
24+
Ok(dir.join("aliases.json"))
25+
}
26+
27+
fn load_store() -> Result<AliasStore> {
28+
let path = aliases_path()?;
29+
if !path.exists() {
30+
return Ok(AliasStore::default());
31+
}
32+
let data = fs::read_to_string(&path)?;
33+
Ok(serde_json::from_str(&data).unwrap_or_default())
34+
}
35+
36+
fn save_store(store: &AliasStore) -> Result<()> {
37+
let path = aliases_path()?;
38+
let data = serde_json::to_string_pretty(store)?;
39+
fs::write(&path, data)?;
40+
Ok(())
41+
}
42+
43+
pub fn save(name: &str, command: &str, description: Option<&str>) -> Result<()> {
44+
let mut store = load_store()?;
45+
store.aliases.insert(
46+
name.to_string(),
47+
AliasEntry {
48+
command: command.to_string(),
49+
description: description.map(|s| s.to_string()),
50+
},
51+
);
52+
save_store(&store)?;
53+
Ok(())
54+
}
55+
56+
pub fn get(name: &str) -> Result<Option<AliasEntry>> {
57+
let store = load_store()?;
58+
Ok(store.aliases.get(name).cloned())
59+
}
60+
61+
pub fn list() -> Result<Vec<(String, AliasEntry)>> {
62+
let store = load_store()?;
63+
let mut entries: Vec<_> = store.aliases.into_iter().collect();
64+
entries.sort_by(|a, b| a.0.cmp(&b.0));
65+
Ok(entries)
66+
}
67+
68+
pub fn remove(name: &str) -> Result<bool> {
69+
let mut store = load_store()?;
70+
let removed = store.aliases.remove(name).is_some();
71+
if removed {
72+
save_store(&store)?;
73+
}
74+
Ok(removed)
75+
}

0 commit comments

Comments
 (0)