Skip to content

Commit

Permalink
Add search subcommand (#13)
Browse files Browse the repository at this point in the history
* Refactor and propagate subcommand errors

* Improve search subcommand

* Add documentation
  • Loading branch information
guywaldman authored Jul 17, 2024
1 parent eb5fdb1 commit fc76563
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 70 deletions.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
![Magic CLI logo](/assets/logo_sm.png)

![GitHub Actions CI](https://github.com/guywaldman/magic-cli/actions/workflows/ci.yml/badge.svg)

<!-- ![homebrew version](https://img.shields.io/homebrew/v/guywaldman%2Ftap%2Fmagic-cli)
![homebrew downloads](https://img.shields.io/homebrew/installs/dm/guywaldman%2Ftap%2Fmagic-cli?label=homebrew%20downloads) -->


Magic CLI is a command line utility which uses LLMs to help you use the command line more efficiently, inspired by projects such as [Amazon Q (prev. Fig terminal)](https://fig.io/) and [GitHub Copilot for CLI](https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line).

> **Read the [announcement blog post](https://guywaldman.com/posts/introducing-magic-cli).**
Expand Down Expand Up @@ -49,11 +49,15 @@ powershell -c "irm https://github.com/guywaldman/magic-cli/releases/download/0.0

See the [releases page](https://github.com/guywaldman/magic-cli/releases) for binaries for your platform.

## Usage Tip

Add a

## Features

- Suggest a command (see [section](#feature-suggest-a-command))
- Ask to generate a command to perform a task (see [section](#feature-ask-to-generate-a-command))
- Semantic search of commands across your shell history
- Use a local or remote LLM (see [section](#use-different-llms))

### Suggest a command
Expand All @@ -75,6 +79,23 @@ Arguments:
<PROMPT> The prompt to suggest a command for (e.g., "Resize image to 300x300 with ffmpeg")
```

### Search across your command history (experimental)

Search a command across your shell history, and get a list of the top results.

![Search subcommand](/assets/search_screenshot.png)

```shell
magic-cli search "zellij attach"
```

```
Usage: magic-cli search [OPTIONS] <PROMPT>
Arguments:
<PROMPT> The prompt to search for
```

### Ask to generate a command (experimental)

Supply a prompt with a task you want the model to perform, and watch it try to suggest a command to run to achieve the goal.
Expand Down Expand Up @@ -140,9 +161,11 @@ The currently suppprted configuration options are:

Security is taken seriously and all vulnerabilities will be handled with utmost care and priority.

In terms of data stored, the only credential that is currently handled by Magic CLI is the OpenAI API key, which is stored in the **user home directory**.
In terms of data stored, the sensitive data that is currently handled by Magic CLI is:

There are plans to store this token in the system's secure key storage, but this is not yet implemented.
- OpenAI API key, which is stored in the configuration within the **user home directory** (`!/.config/magic_cli`).
> There are plans to store this token in the system's secure key storage, but this is not yet implemented.
- Embeddings of the shell history for the `magic-cli search` command, which are stored in the configuration within the **user home directory** (`!/.config/magic_cli`)

Please see [SECURITY.md](SECURITY.md) for more information, and instructions on how to report potential vulnerabilities.

Expand Down
Binary file added assets/search_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 17 additions & 15 deletions src/cli/mcli.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use super::{
subcommand::MagicCliSubcommand,
subcommand_ask::AskSubcommand,
subcommand_config::{ConfigSubcommand, ConfigSubcommands},
subcommand_search::SearchSubcommand,
subcommand_suggest::SuggestSubcommand,
subcommand_sysinfo::SysInfoSubcommand,
};
use clap::{ArgAction, Parser, Subcommand};
use colored::Colorize;
use std::{error::Error, process::exit};

#[derive(Parser)]
Expand Down Expand Up @@ -57,32 +59,32 @@ impl MagicCli {

match clap_cli.command {
Commands::Suggest { prompt } => {
if SuggestSubcommand::run(&prompt).is_err() {
exit(1);
}
Self::run_subcommmand(SuggestSubcommand::new(prompt));
}
Commands::Ask { prompt } => {
if AskSubcommand::run(&prompt).is_err() {
exit(1);
}
Self::run_subcommmand(AskSubcommand::new(prompt));
}
Commands::Config { command } => {
if ConfigSubcommand::run(&command).is_err() {
exit(1);
}
Self::run_subcommmand(ConfigSubcommand::new(command));
}
Commands::Search { prompt, index } => {
if SearchSubcommand::run(&prompt, index).is_err() {
exit(1);
}
Self::run_subcommmand(SearchSubcommand::new(prompt, index));
}
Commands::SysInfo => {
if SysInfoSubcommand::run().is_err() {
exit(1);
}
Self::run_subcommmand(SysInfoSubcommand::new());
}
}

Ok(())
}

fn run_subcommmand(subcommand: impl MagicCliSubcommand) {
match subcommand.run() {
Ok(_) => {}
Err(err) => {
eprintln!("{}", format!("Error: {}", err).red().bold());
exit(1);
}
}
}
}
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod command;
mod config;
mod mcli;
mod search;
mod subcommand;
mod subcommand_ask;
mod subcommand_config;
mod subcommand_search;
Expand Down
4 changes: 4 additions & 0 deletions src/cli/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ impl CliSearch {
let semantic_search_engine = SemanticSearchEngine::new(dyn_clone::clone_box(&*self.llm));
let semantic_search_results = semantic_search_engine.top_k(prompt, index, 10)?;

if semantic_search_results.is_empty() {
println!("{}", "No relevant results found.".yellow().bold());
}

let options = semantic_search_results
.iter()
.map(|result| ListOption::new(result.id, result.data.clone()))
Expand Down
5 changes: 5 additions & 0 deletions src/cli/subcommand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use std::error::Error;

pub trait MagicCliSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>>;
}
70 changes: 39 additions & 31 deletions src/cli/subcommand_ask.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,49 @@
use super::{command::CliCommand, config::MagicCliConfig};
use super::{command::CliCommand, config::MagicCliConfig, subcommand::MagicCliSubcommand};
use crate::{
cli::command::CommandRunResult,
core::{AskEngine, AskResponse},
};
use colored::Colorize;
use std::error::Error;

pub struct AskSubcommand;
pub struct AskSubcommand {
prompt: String,
}

impl AskSubcommand {
pub fn run(prompt: &str) -> Result<(), Box<dyn Error>> {
pub fn new(prompt: String) -> Self {
Self { prompt }
}

fn create_context(history: &[String]) -> String {
history.iter().map(|item| format!("- {}", item)).collect::<Vec<_>>().join("\n")
}

fn print_response(response: &AskResponse) {
match response {
AskResponse::Suggest(suggest_response) => {
println!("{}", "Suggestion:".green().bold());
println!("{}", format!(" - Command: {}", suggest_response.command.blue().bold()));
println!("{}", format!(" - Explanation: {}", suggest_response.explanation.italic()));
}
AskResponse::Ask(ask_response) => {
println!("{}", "Action required:".yellow().bold());
println!("{}", format!(" - Command: {}", ask_response.command.blue().bold()));
println!("{}", format!(" - Rationale: {}", ask_response.rationale.italic()));
}
AskResponse::Success(success_response) => {
println!("{}", "Success:".green().bold());
println!(
"{}",
format!(" - Success: {}", success_response.success.to_string().green().bold())
);
}
}
}
}

impl MagicCliSubcommand for AskSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>> {
let config = MagicCliConfig::load_config()?;
let llm = MagicCliConfig::llm_from_config(&config)?;
println!("{}", "Model details:".dimmed());
Expand All @@ -22,9 +56,9 @@ impl AskSubcommand {

println!("\nGenerating initial response from model...");

let mut history: Vec<String> = vec![format!("User has requested the ask '{}'.", prompt)];
let mut history: Vec<String> = vec![format!("User has requested the ask '{}'.", self.prompt)];
let ask_engine = AskEngine::new(llm);
let mut command = ask_engine.ask_command(prompt)?;
let mut command = ask_engine.ask_command(&self.prompt)?;
loop {
Self::print_response(&command);
match command {
Expand Down Expand Up @@ -68,30 +102,4 @@ impl AskSubcommand {
println!("{}", "Successfully completed the ask".green().bold());
Ok(())
}

fn create_context(history: &[String]) -> String {
history.iter().map(|item| format!("- {}", item)).collect::<Vec<_>>().join("\n")
}

fn print_response(response: &AskResponse) {
match response {
AskResponse::Suggest(suggest_response) => {
println!("{}", "Suggestion:".green().bold());
println!("{}", format!(" - Command: {}", suggest_response.command.blue().bold()));
println!("{}", format!(" - Explanation: {}", suggest_response.explanation.italic()));
}
AskResponse::Ask(ask_response) => {
println!("{}", "Action required:".yellow().bold());
println!("{}", format!(" - Command: {}", ask_response.command.blue().bold()));
println!("{}", format!(" - Rationale: {}", ask_response.rationale.italic()));
}
AskResponse::Success(success_response) => {
println!("{}", "Success:".green().bold());
println!(
"{}",
format!(" - Success: {}", success_response.success.to_string().green().bold())
);
}
}
}
}
19 changes: 15 additions & 4 deletions src/cli/subcommand_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ use clap::Subcommand;
use colored::Colorize;
use inquire::Text;

use super::config::{ConfigKeys, MagicCliConfig};
use super::{
config::{ConfigKeys, MagicCliConfig},
subcommand::MagicCliSubcommand,
};

#[derive(Subcommand)]
pub enum ConfigSubcommands {
Expand All @@ -31,11 +34,19 @@ pub enum ConfigSubcommands {
Path,
}

pub struct ConfigSubcommand;
pub struct ConfigSubcommand {
command: ConfigSubcommands,
}

impl ConfigSubcommand {
pub fn run(command: &ConfigSubcommands) -> Result<(), Box<dyn Error>> {
match command {
pub fn new(command: ConfigSubcommands) -> Self {
Self { command }
}
}

impl MagicCliSubcommand for ConfigSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>> {
match &self.command {
ConfigSubcommands::Set { key, value } => {
let key = match key {
Some(key) => key.to_string(),
Expand Down
17 changes: 13 additions & 4 deletions src/cli/subcommand_search.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
use std::error::Error;

use super::{command::CliCommand, config::MagicCliConfig, search::CliSearch};
use super::{command::CliCommand, config::MagicCliConfig, search::CliSearch, subcommand::MagicCliSubcommand};

pub struct SearchSubcommand;
pub struct SearchSubcommand {
prompt: String,
index: bool,
}

impl SearchSubcommand {
pub fn run(prompt: &str, index: bool) -> Result<(), Box<dyn Error>> {
pub fn new(prompt: String, index: bool) -> Self {
Self { prompt, index }
}
}

impl MagicCliSubcommand for SearchSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>> {
let config = MagicCliConfig::load_config()?;
let llm = MagicCliConfig::llm_from_config(&config)?;
let cli_search = CliSearch::new(llm);
let selected_command = cli_search.search_command(prompt, index)?;
let selected_command = cli_search.search_command(&self.prompt, self.index)?;

let config = MagicCliConfig::load_config()?;
CliCommand::new(config.suggest).suggest_user_action_on_command(&selected_command)?;
Expand Down
16 changes: 12 additions & 4 deletions src/cli/subcommand_suggest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ use std::error::Error;

use crate::core::SuggestionEngine;

use super::{command::CliCommand, config::MagicCliConfig};
use super::{command::CliCommand, config::MagicCliConfig, subcommand::MagicCliSubcommand};

pub struct SuggestSubcommand;
pub struct SuggestSubcommand {
prompt: String,
}

impl SuggestSubcommand {
pub fn run(prompt: &str) -> Result<(), Box<dyn Error>> {
pub fn new(prompt: String) -> Self {
Self { prompt }
}
}

impl MagicCliSubcommand for SuggestSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>> {
let config = MagicCliConfig::load_config()?;
let llm = MagicCliConfig::llm_from_config(&config)?;
let explain_subcommand = SuggestionEngine::new(llm);
let command = explain_subcommand.suggest_command(prompt)?;
let command = explain_subcommand.suggest_command(&self.prompt)?;
CliCommand::new(config.suggest).suggest_user_action_on_command(&command)?;
Ok(())
}
Expand Down
10 changes: 9 additions & 1 deletion src/cli/subcommand_sysinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ use colored::Colorize;

use crate::core::Shell;

use super::subcommand::MagicCliSubcommand;

pub struct SysInfoSubcommand;

impl SysInfoSubcommand {
pub fn run() -> Result<(), Box<dyn Error>> {
pub fn new() -> Self {
Self {}
}
}

impl MagicCliSubcommand for SysInfoSubcommand {
fn run(&self) -> Result<(), Box<dyn Error>> {
let sysinfo = Shell::extract_env_info()?;
println!("System information as detected by the CLI:");
println!();
Expand Down
7 changes: 7 additions & 0 deletions src/core/semantic_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub struct SemanticSearchEngine {
}

impl SemanticSearchEngine {
const SIMILARITY_THRESHOLD: f64 = 0.2;

pub fn new(llm: Box<dyn Llm>) -> Self {
Self { llm }
}
Expand All @@ -47,6 +49,11 @@ impl SemanticSearchEngine {
.iter()
.filter_map(|item| {
let similarity = f32::cosine(item.embedding.as_slice(), needle_embedding.as_slice());

if similarity.map(|score| score < Self::SIMILARITY_THRESHOLD).unwrap_or(false) {
return None;
}

similarity.map(|score| SemanticSearchResult {
id: item.item.id,
data: item.item.data.clone(),
Expand Down
Loading

0 comments on commit fc76563

Please sign in to comment.