aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/commands.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-02-24 11:48:20 -0800
committerGitHub <noreply@github.com>2026-02-24 11:48:20 -0800
commit6ea760bb6b36da241961e8ecd60cb2c5e15c0a78 (patch)
tree18ebbb710cea24e30bc69b5d6bc807518a950746 /crates/atuin-ai/src/commands.rs
parentfix: forward $PATH to tmux popup in zsh (#3198) (diff)
downloadatuin-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.rs')
-rw-r--r--crates/atuin-ai/src/commands.rs71
1 files changed, 58 insertions, 13 deletions
diff --git a/crates/atuin-ai/src/commands.rs b/crates/atuin-ai/src/commands.rs
index 56741544..7d5ca16b 100644
--- a/crates/atuin-ai/src/commands.rs
+++ b/crates/atuin-ai/src/commands.rs
@@ -1,7 +1,11 @@
+use atuin_common::shell::Shell;
use clap::{Parser, Subcommand};
use tracing::Level;
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
+#[cfg(debug_assertions)]
+pub mod debug_render;
+
pub mod init;
pub mod inline;
@@ -16,6 +20,10 @@ struct Cli {
#[arg(long, global = true, env = "ATUIN_AI_API_ENDPOINT")]
api_endpoint: Option<String>,
+ /// Custom API token
+ #[arg(long, global = true, env = "ATUIN_AI_API_TOKEN")]
+ api_token: Option<String>,
+
#[command(subcommand)]
command: Commands,
}
@@ -23,13 +31,10 @@ struct Cli {
#[derive(Subcommand, Debug)]
enum Commands {
/// Initialize shell integration
- Init,
-
- /// Complete current command line
- Complete {
- /// Current command line to complete
- #[arg(value_name = "COMMAND")]
- command: Option<String>,
+ Init {
+ /// Shell to generate integration for; defaults to "auto"
+ #[arg(value_name = "SHELL", default_value = "auto")]
+ shell: String,
},
/// Inline completion mode with small TUI overlay
@@ -41,10 +46,27 @@ enum Commands {
/// Start in natural language mode
#[arg(long)]
natural_language: bool,
+
+ /// Keep TUI output visible after exit (default: erase)
+ #[arg(long)]
+ keep: bool,
+
+ /// Log state changes to file for debugging (dev tool)
+ #[arg(long, value_name = "FILE")]
+ debug_state: Option<String>,
},
- /// Interactive mode with TUI
- Interactive,
+ /// Debug render: output a single frame from JSON state (dev tool)
+ #[cfg(debug_assertions)]
+ DebugRender {
+ /// Input file (reads from stdin if not provided)
+ #[arg(short, long)]
+ input: Option<String>,
+
+ /// Output format: ansi (default), plain, json
+ #[arg(short, long, default_value = "ansi")]
+ format: String,
+ },
}
pub async fn run() -> eyre::Result<()> {
@@ -53,13 +75,32 @@ pub async fn run() -> eyre::Result<()> {
init_tracing(cli.verbose);
match cli.command {
- Commands::Init => init::run().await,
+ Commands::Init { shell } => init::run(shell).await,
Commands::Inline {
command,
natural_language,
- } => inline::run(command, natural_language, cli.api_endpoint).await,
- Commands::Complete { command } => inline::run(command, false, cli.api_endpoint).await,
- Commands::Interactive => Err(eyre::eyre!("interactive mode not implemented yet")),
+ keep,
+ debug_state,
+ } => {
+ inline::run(
+ command,
+ natural_language,
+ cli.api_endpoint,
+ cli.api_token,
+ keep,
+ debug_state,
+ )
+ .await
+ }
+ #[cfg(debug_assertions)]
+ Commands::DebugRender { input, format } => {
+ let output_format = match format.as_str() {
+ "plain" => debug_render::OutputFormat::Plain,
+ "json" => debug_render::OutputFormat::Json,
+ _ => debug_render::OutputFormat::Ansi,
+ };
+ debug_render::run(input, output_format).await
+ }
}
}
@@ -95,3 +136,7 @@ fn init_tracing(verbose: bool) {
subscriber.init();
}
}
+
+pub fn detect_shell() -> Option<String> {
+ Some(Shell::current().to_string())
+}