diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-02-24 11:48:20 -0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-24 11:48:20 -0800 |
| commit | 6ea760bb6b36da241961e8ecd60cb2c5e15c0a78 (patch) | |
| tree | 18ebbb710cea24e30bc69b5d6bc807518a950746 /crates/atuin-ai/src/commands/init.rs | |
| parent | fix: forward $PATH to tmux popup in zsh (#3198) (diff) | |
| download | atuin-6ea760bb6b36da241961e8ecd60cb2c5e15c0a78.zip | |
feat: Generate commands or ask questions with `atuin ai` (#3199)
This PR refines the system created in #3178 to be suitable for a v1
release.
---
## Overview
`atuin-ai` is a separate binary that allows for generating commands and
asking questions from the command line.
It is fully opt-in.
## Usage
`atuin ai init` will output bindings for your shell. Currently, bash,
zsh, and fish are supported.
```bash
eval "$(atuin ai init)"
```
Once the hooks are installed, just press `?` on an empty prompt line to
call up the TUI.
`atuin ai` requires an account on [Atuin Hub](https://hub.atuin.sh/);
you will be prompted to log in on first use.
## Features
### Command generation
Prompt the LLM to create a command, and get one back, no fuss. Press
`enter` to run, or `tab` to insert.
```
┌Ask questions or generate a command:──────────────────────────┐
│ │
│ > Get a list of running docker containers │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ docker ps │
│ │
└────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘
```
### Follow-up
You can follow-up with `f` to specify a refinement prompt to update the
command that will be inserted.
```
┌Ask questions or generate a command:──────────────────────────┐
│ │
│ > Get a list of running docker containers │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ docker ps │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ > Actually I want to get all docker containers │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ docker ps -a │
│ │
└────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘
```
You can also follow-up with questions to get responses in natural
language.
```
┌Ask questions or generate a command:──────────────────────────┐
│ │
│ > Get a list of running docker containers │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ docker ps │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ > Actually I want to get all docker containers │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ docker ps -a │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ > What other useful flags to `docker ps` should I know? │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ Here are some handy `docker ps` flags: │
│ │
│ - `-q` — Only show container IDs (great for piping to │
│ other commands) │
│ - `-s` — Show container sizes │
│ - `-n 5` — Show the last 5 created containers │
│ - `-l` — Show only the latest created container │
│ - `--no-trunc` — Don't truncate output (shows full IDs and │
│ commands) │
│ - `-f` or `--filter` — Filter by condition, e.g.: │
│ - `-f status=exited` — only exited containers │
│ - `-f name=myapp` — filter by name │
│ - `-f ancestor=nginx` — filter by image │
│ - `--format` — Custom output using Go templates, e.g.: │
│ `--format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"` │
│ │
│ A common combo is `docker ps -aq` to get all container │
│ IDs, useful for bulk operations like `docker rm $(docker │
│ ps -aq)`. │
│ │
└────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘
```
You can use `enter` or `tab` at any time to run or insert the last
suggested command, even if it was suggested in a previous turn.
### Conversational and search usage
If you prompt the LLM with a question that doesn't imply you want to
generate a command, it can respond in natural language, and use web
search if necessary to fetch the data it needs.
```
┌Ask questions or generate a command:──────────────────────────┐
│ │
│ > What is the latest version of atuin? │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ ✓ Used 2 tools │
│ │
│ The latest version of Atuin is **v18.12.0**, available on │
│ the [GitHub releases │
│ page](https://github.com/atuinsh/atuin/releases). │
│ │
└─────────────────────────────────[f]: Follow-up [Esc]: Cancel┘
```
### Dangerous or low-confidence command detection
The LLM scores its confidence in the command, as well as how dangerous
the command is. This information is shown if a threshold is exceeded,
and requires an extra confirmation step before running automatically
with `enter`.
The Atuin Hub server also monitors suggested commands for dangerous
patterns the LLM didn't catch, and appends its own assessment at the end
of the LLM's own assessment.
```
┌Ask questions or generate a command:──────────────────────────┐
│ │
│ > Delete all files from $HOME │
│ │
├──────────────────────────────────────────────────────────────┤
│ │
│ $ rm -rf $HOME/* │
│ │
│ ! ⚠️ This will PERMANENTLY delete ALL files and directories │
│ in your home directory, including documents, downloads, │
│ configurations, SSH keys, and everything else. This is │
│ irreversible and will likely break your system. Also note │
│ this won't delete hidden (dot) files — if you want those │
│ too, that's even more destructive.; [Server] Recursive │
│ delete of critical directory │
│ │
└────[Enter]: Run [Tab]: Insert [f]: Follow-up [Esc]: Cancel┘
```
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'crates/atuin-ai/src/commands/init.rs')
| -rw-r--r-- | crates/atuin-ai/src/commands/init.rs | 155 |
1 files changed, 152 insertions, 3 deletions
diff --git a/crates/atuin-ai/src/commands/init.rs b/crates/atuin-ai/src/commands/init.rs index bf5c6256..8174b583 100644 --- a/crates/atuin-ai/src/commands/init.rs +++ b/crates/atuin-ai/src/commands/init.rs @@ -1,9 +1,29 @@ -pub async fn run() -> eyre::Result<()> { - let zsh_function = generate_zsh_integration(); - println!("{}", zsh_function); +use crate::commands::detect_shell; + +pub async fn run(shell: String) -> eyre::Result<()> { + let integration = match shell.as_str() { + "zsh" => generate_zsh_integration(), + "bash" => generate_bash_integration(), + "fish" => generate_fish_integration(), + "auto" => generate_auto_integration()?, + _ => eyre::bail!("Unsupported shell: {}", shell), + }; + + println!("{}", integration); Ok(()) } +fn generate_auto_integration() -> eyre::Result<&'static str> { + let shell = detect_shell(); + match shell.as_deref() { + Some("zsh") => Ok(generate_zsh_integration()), + Some("bash") => Ok(generate_bash_integration()), + Some("fish") => Ok(generate_fish_integration()), + Some(s) => eyre::bail!("Unsupported shell: {}", s), + None => eyre::bail!("Could not detect shell"), + } +} + /// Generate the zsh integration function - pure function for easy testing pub fn generate_zsh_integration() -> &'static str { r#" @@ -53,6 +73,111 @@ bindkey '?' _atuin_ai_question_mark # Question mark .trim() } +/// Generate the bash integration function - pure function for easy testing +pub fn generate_bash_integration() -> &'static str { + r#" +# Question mark at start of line - natural language mode +_atuin_ai_question_mark() { + # If buffer is empty or just contains '?', trigger natural language mode + if [[ -z "$READLINE_LINE" || "$READLINE_LINE" == "?" ]]; then + READLINE_LINE="" + READLINE_POINT=0 + + local output + output=$(atuin-ai inline --natural-language 3>&1 1>&2 2>&3) + + if [[ $output == __atuin_ai_cancel__ ]]; then + # User cancelled, do nothing + READLINE_LINE="" + READLINE_POINT=0 + elif [[ $output == __atuin_ai_execute__:* ]]; then + # Execute the command immediately + READLINE_LINE=${output#__atuin_ai_execute__:} + READLINE_POINT=${#READLINE_LINE} + # Note: We can't directly execute in bash bind -x, but we can + # use a workaround by binding to a macro that accepts the line + bind '"\C-x\C-a": accept-line' + bind -x '"\C-x\C-e": _atuin_ai_question_mark' + elif [[ $output == __atuin_ai_insert__:* ]]; then + # Insert the command for editing + READLINE_LINE=${output#__atuin_ai_insert__:} + READLINE_POINT=${#READLINE_LINE} + elif [[ -n $output ]]; then + # Default: insert for editing + READLINE_LINE=$output + READLINE_POINT=${#READLINE_LINE} + fi + else + # Not at empty prompt, just insert the question mark + READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}?${READLINE_LINE:READLINE_POINT}" + ((READLINE_POINT++)) + fi +} + +# Set up keybindings +# Bash requires special handling: we use bind -x for the function, +# but need a two-step approach for execute mode +__atuin_ai_accept_line="" + +_atuin_ai_question_mark_wrapper() { + _atuin_ai_question_mark + if [[ -n "$__atuin_ai_accept_line" ]]; then + __atuin_ai_accept_line="" + fi +} + +bind -x '"?": _atuin_ai_question_mark' +"# + .trim() +} + +/// Generate the fish integration function - pure function for easy testing +pub fn generate_fish_integration() -> &'static str { + r#" +# Question mark at start of line - natural language mode +function _atuin_ai_question_mark + set -l buf (commandline -b) + + # If buffer is empty or just contains '?', trigger natural language mode + if test -z "$buf" -o "$buf" = "?" + commandline -r "" + + # Run atuin-ai inline, swapping stdout and stderr + set -l output (atuin-ai inline --natural-language 3>&1 1>&2 2>&3 | string collect) + + if test "$output" = "__atuin_ai_cancel__" + # User cancelled, do nothing + commandline -f repaint + else if string match --quiet '__atuin_ai_execute__:*' "$output" + # Execute the command immediately + set -l cmd (string replace "__atuin_ai_execute__:" "" -- "$output" | string collect) + commandline -r "$cmd" + commandline -f repaint + commandline -f execute + else if string match --quiet '__atuin_ai_insert__:*' "$output" + # Insert the command for editing + set -l cmd (string replace "__atuin_ai_insert__:" "" -- "$output" | string collect) + commandline -r "$cmd" + commandline -f repaint + else if test -n "$output" + # Default: insert for editing + commandline -r "$output" + commandline -f repaint + else + commandline -f repaint + end + else + # Not at empty prompt, just insert the question mark + commandline -i "?" + end +end + +# Set up keybindings +bind "?" _atuin_ai_question_mark +"# + .trim() +} + #[cfg(test)] mod tests { use super::*; @@ -67,4 +192,28 @@ mod tests { assert!(result.contains("__atuin_ai_execute__")); assert!(result.contains("__atuin_ai_insert__")); } + + #[test] + fn test_generate_bash_integration() { + let result = generate_bash_integration(); + assert!(result.contains("_atuin_ai_question_mark")); + assert!(result.contains("bind")); + assert!(result.contains("READLINE_LINE")); + assert!(result.contains("atuin-ai inline")); + assert!(result.contains("__atuin_ai_cancel__")); + assert!(result.contains("__atuin_ai_execute__")); + assert!(result.contains("__atuin_ai_insert__")); + } + + #[test] + fn test_generate_fish_integration() { + let result = generate_fish_integration(); + assert!(result.contains("_atuin_ai_question_mark")); + assert!(result.contains("bind")); + assert!(result.contains("commandline")); + assert!(result.contains("atuin-ai inline")); + assert!(result.contains("__atuin_ai_cancel__")); + assert!(result.contains("__atuin_ai_execute__")); + assert!(result.contains("__atuin_ai_insert__")); + } } |
