diff --git a/tiles/Cargo.toml b/tiles/Cargo.toml index 91aaf8b..0b49162 100644 --- a/tiles/Cargo.toml +++ b/tiles/Cargo.toml @@ -47,6 +47,7 @@ serde_ipld_dagcbor = "0.6.4" cid = "0.11.3" keyring-core = "1.0.0" iroh-mdns-address-lookup = "0.4.0" +tempfile = "3" [target.'cfg(target_os = "linux")'.dependencies] dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] } @@ -55,7 +56,6 @@ dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rus apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] } [dev-dependencies] -tempfile = "3" keyring-core = { version = "1.0.0", features = ["sample"] } wiremock = "0.6.5" async-std = { version = "1.12", features = ["attributes"] } diff --git a/tiles/src/core/plugin.rs b/tiles/src/core/plugin.rs index 02859af..1254779 100644 --- a/tiles/src/core/plugin.rs +++ b/tiles/src/core/plugin.rs @@ -13,8 +13,12 @@ use std::{ use anyhow::{Context, Result, anyhow}; use log::info; use reqwest::Client; +use tempfile::tempdir; -use crate::utils::config::{ConfigProvider, DefaultProvider}; +use crate::utils::{ + config::{ConfigProvider, DefaultProvider}, + copy_recursive, +}; pub async fn install(path: String) -> Result { if let Ok(url) = reqwest::Url::parse(&path) { @@ -72,25 +76,40 @@ fn install_from_local_source(local_path: PathBuf) -> Result { let pi_skills_dir = user_data_dir.join("pi/agent/skills"); std::fs::create_dir_all(&pi_skills_dir).context("Failed to create Pi skills directory")?; + let tmp_dir = tempdir().expect("Failed to create tmp dir"); + + let mut tmp_path = tmp_dir.path().to_path_buf(); + + tmp_path.push("tmp_tiles_plugins"); + std::fs::create_dir_all(&tmp_path) + .context("Failed to create temporary plugins directory")?; + let output = if local_path.ends_with(".zip") { Command::new("unzip") .arg(&local_path) .arg("-d") - .arg(pi_skills_dir) + .arg(&tmp_path) .output()? } else { Command::new("tar") .arg("-xzf") .arg(&local_path) .arg("-C") - .arg(pi_skills_dir) + .arg(&tmp_path) .output()? }; + if !output.status.success() { let output_str = String::from_utf8_lossy(&output.stderr); Err(anyhow!("{}", output_str)) } else { - Ok(format!("Successfully installed plugin {}", plugin_name)) + // For now we are only copying skills + if tmp_path.join(plugin_name).join("skills").is_dir() { + copy_recursive(&tmp_path.join(plugin_name).join("skills"), &pi_skills_dir)?; + Ok(format!("Successfully installed plugin {}", plugin_name)) + } else { + Ok("Skills not found in this plugin".to_string()) + } } } else { Err(anyhow!( @@ -105,9 +124,6 @@ fn is_valid_file_by_extension(filename: &str) -> bool { } pub fn uninstall(plugin_name: &str) -> Result { - // find if the skill is there - // if else do rm -rf - // let user_data_dir = DefaultProvider.get_user_data_dir()?; let pi_skills_dir = user_data_dir.join("pi/agent/skills"); @@ -146,12 +162,7 @@ pub fn list() -> Result<()> { for dir_result in read_dir { match dir_result { Ok(dir) => { - println!( - "{}", - dir.file_name() - .to_str() - .expect("Conversino from Osstr to str failed") - ) + println!("{}", dir.file_name().to_string_lossy()) } Err(_err) => continue, } diff --git a/tiles/src/main.rs b/tiles/src/main.rs index 9226ff5..88e220a 100644 --- a/tiles/src/main.rs +++ b/tiles/src/main.rs @@ -61,7 +61,8 @@ const CLI_HELP_TEMPLATE: &str = concat!( " update Update Tiles to the latest version\n", " health Check the status of dependencies\n", " server Configure the inference server\n", - " daemon Configure daemon behavior\n", + " daemon Configure daemon behavior\n\n", + " Tools\n", " plugin Manage plugins such as skills, extensions etc\n\n", "Options:\n", " -h, --help Show help\n", @@ -101,6 +102,9 @@ enum Commands { #[command(flatten, next_help_heading = "System")] System(SystemCommands), + + #[command(flatten, next_help_heading = "Plugins")] + Tools(ToolsCommandsGroup), } #[derive(Debug, Subcommand)] @@ -159,9 +163,6 @@ enum SystemCommands { /// Configure daemon behavior Daemon(DaemonArgs), - - /// Manages plugins such as skills, extensions etc.. - Plugin(PluginArgs), } #[derive(Debug, Subcommand)] @@ -297,6 +298,11 @@ enum DaemonCommands { Stop, } +#[derive(Debug, Subcommand)] +enum ToolsCommandsGroup { + /// Manage Plugins + Plugin(PluginArgs), +} #[derive(Debug, Args)] #[command(args_conflicts_with_subcommands = true)] #[command(flatten_help = true)] @@ -457,24 +463,26 @@ pub async fn main() -> Result<(), Box> { .inspect(|_| println!("Daemon stopped successfully"))?, _ => start_server(None).await?, }, - Some(Commands::System(SystemCommands::Plugin(plugin_args))) => match plugin_args.command { - PluginCommands::List => { - plugin::list()?; - } - PluginCommands::Install { path } => match install(path).await { - Ok(resp) => println!("{}", resp), - Err(err) => println!("Plugin failed to install due to {:?}", err), - }, - PluginCommands::Uninstall { name } => { - // handle uninstall - match uninstall(&name) { + Some(Commands::Tools(ToolsCommandsGroup::Plugin(plugin_args))) => { + match plugin_args.command { + PluginCommands::List => { + plugin::list()?; + } + PluginCommands::Install { path } => match install(path).await { Ok(resp) => println!("{}", resp), - Err(_err) => eprintln!( - "Plugin failed to uninstall, please check if the name is correct and try again" - ), + Err(err) => eprintln!("Plugin failed to install due to {:?}", err), + }, + PluginCommands::Uninstall { name } => { + // handle uninstall + match uninstall(&name) { + Ok(resp) => println!("{}", resp), + Err(_err) => eprintln!( + "Plugin failed to uninstall, please check if the name is correct and try again" + ), + } } } - }, + } Some(Commands::Sync(SyncCommands::Link(link_args))) => match link_args.command { LinkCommands::Revoke { peer_did } => unlink_peer(&db_conn, &peer_did)?, diff --git a/tiles/src/repl.rs b/tiles/src/repl.rs index 71b5b8d..9d817c0 100644 --- a/tiles/src/repl.rs +++ b/tiles/src/repl.rs @@ -374,6 +374,7 @@ enum InputType { Command(String), Exit, Prompt, + Skill, } #[derive(Deserialize, Serialize, Debug)] @@ -469,9 +470,10 @@ fn handle_input(input: &str) -> InputType { println!("Empty command. Type /help for available commands."); InputType::Skip } - cmd if cmd.starts_with("skill:") => InputType::Prompt, cmd => InputType::Command(cmd.to_owned()), } + } else if let Some(_skill) = input.strip_prefix('$') { + InputType::Skill } else { InputType::Prompt } @@ -515,10 +517,10 @@ fn show_help() { ], ), ( - "Tools", + "Plugins", vec![ ("/skills", "List all the available skills"), - ("/skill:", "Use the skill directly"), + ("$", "Use the skill directly"), ], ), ]; @@ -624,6 +626,11 @@ async fn start_repl(modelfile: &Modelfile, _run_args: &RunArgs, db_conn: &Dbconn InputType::Prompt => { handle_input_prompt(pi_stdin, &mut repl_session, &input).await?; } + InputType::Skill => { + let (_, skill_name) = input.split_at(1); + let skill_prompt = format!("/skill:{}", skill_name); + handle_input_prompt(pi_stdin, &mut repl_session, &skill_prompt).await?; + } InputType::Command(cmd) => { let res = handle_input_commands(cmd, &mut repl_session, db_conn, pi_stdin).await?; @@ -921,7 +928,15 @@ async fn process_command( let mut index = 0; commands.iter().for_each(|cmd| { index += 1; - println!("{}. {} - {}", index, cmd.name, cmd.description); + // chucking off `skill:` from the name + let (_, skill_name) = cmd.name.split_at(6); + println!( + "{}. {}{} - {}", + index.purple(), + "$".yellow(), + skill_name.bright_green(), + cmd.description.bright_cyan() + ); }); } else { println!("No commands found") diff --git a/tiles/src/utils/mod.rs b/tiles/src/utils/mod.rs index 71482e7..8b57362 100644 --- a/tiles/src/utils/mod.rs +++ b/tiles/src/utils/mod.rs @@ -1,4 +1,8 @@ -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, +}; pub mod config; pub mod crypto; @@ -14,3 +18,57 @@ pub fn get_unix_time_now() -> u64 { pub fn test_logger() { let _ = env_logger::builder().is_test(true).try_init(); } + +pub fn copy_recursive(src: &PathBuf, dest: &PathBuf) -> anyhow::Result<()> { + fs::create_dir_all(dest)?; + + let ls = fs::read_dir(src)?; + + for dir_result in ls { + let dir_path = dir_result?; + let file_type = dir_path.file_type()?; + let filename = dir_path.file_name(); + let src_path = dir_path.path(); + let dest_path = PathBuf::new().join(dest).join(filename); + if file_type.is_dir() { + copy_recursive(&src_path, &dest_path)? + } else { + fs::copy(&src_path, &dest_path)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::fs; + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_valid_copy() { + let tmp = tempdir().expect("created tmp dir"); + + let src = tmp.path().join("source"); + let dest = tmp.path().join("dest"); + + fs::create_dir_all(src.join("skills").join("flamez")).unwrap(); + + fs::write( + src.join("skills").join("flamez").join("SKILLS.md"), + "skill".as_bytes(), + ) + .unwrap(); + + copy_recursive(&src, &dest).unwrap(); + + assert!( + dest.join("skills") + .join("flamez") + .join("SKILLS.md") + .exists() + ) + } +}