A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Added basic DID based identity implementation in Tiles

Added following commands,
- `account` - shows the account details from toml
- `account create <nickname>` - nickname optional
- `account set-nickname` - set nickname to the root account

The root-user details are stored under config.toml under config dir

madclaws 56c771d7 e32cc9a3

+358 -10
+31 -5
Cargo.lock
··· 2370 2370 "spin", 2371 2371 "tokio", 2372 2372 "tokio-util", 2373 - "toml", 2373 + "toml 0.9.11+spec-1.1.0", 2374 2374 "tracing", 2375 2375 "tracing-subscriber", 2376 2376 ] ··· 3963 3963 "clap", 3964 3964 "futures-util", 3965 3965 "hf-hub", 3966 + "keyring", 3966 3967 "owo-colors", 3967 3968 "reqwest", 3968 3969 "rustyline", ··· 3971 3972 "tempfile", 3972 3973 "tilekit", 3973 3974 "tokio", 3975 + "toml 1.0.3+spec-1.1.0", 3974 3976 ] 3975 3977 3976 3978 [[package]] ··· 4062 4064 "indexmap", 4063 4065 "serde_core", 4064 4066 "serde_spanned", 4065 - "toml_datetime", 4067 + "toml_datetime 0.7.5+spec-1.1.0", 4068 + "toml_parser", 4069 + "toml_writer", 4070 + "winnow", 4071 + ] 4072 + 4073 + [[package]] 4074 + name = "toml" 4075 + version = "1.0.3+spec-1.1.0" 4076 + source = "registry+https://github.com/rust-lang/crates.io-index" 4077 + checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" 4078 + dependencies = [ 4079 + "indexmap", 4080 + "serde_core", 4081 + "serde_spanned", 4082 + "toml_datetime 1.0.0+spec-1.1.0", 4066 4083 "toml_parser", 4067 4084 "toml_writer", 4068 4085 "winnow", ··· 4078 4095 ] 4079 4096 4080 4097 [[package]] 4098 + name = "toml_datetime" 4099 + version = "1.0.0+spec-1.1.0" 4100 + source = "registry+https://github.com/rust-lang/crates.io-index" 4101 + checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" 4102 + dependencies = [ 4103 + "serde_core", 4104 + ] 4105 + 4106 + [[package]] 4081 4107 name = "toml_edit" 4082 4108 version = "0.23.10+spec-1.0.0" 4083 4109 source = "registry+https://github.com/rust-lang/crates.io-index" 4084 4110 checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" 4085 4111 dependencies = [ 4086 4112 "indexmap", 4087 - "toml_datetime", 4113 + "toml_datetime 0.7.5+spec-1.1.0", 4088 4114 "toml_parser", 4089 4115 "winnow", 4090 4116 ] 4091 4117 4092 4118 [[package]] 4093 4119 name = "toml_parser" 4094 - version = "1.0.6+spec-1.1.0" 4120 + version = "1.0.9+spec-1.1.0" 4095 4121 source = "registry+https://github.com/rust-lang/crates.io-index" 4096 - checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" 4122 + checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" 4097 4123 dependencies = [ 4098 4124 "winnow", 4099 4125 ]
-2
tilekit/src/modelfile.rs
··· 341 341 .parse(input) 342 342 } 343 343 fn create_modelfile(commands: Vec<(&str, Output)>) -> Result<Modelfile, String> { 344 - // TODO: There might be a better way 345 344 let mut modelfile: Modelfile = Modelfile::new(); 346 345 for command in commands { 347 346 let _ = match (command.0.to_lowercase().as_str(), command.1) { 348 - //TODO: Can add validations for path if its a gguf file later 349 347 ("from", Output::Single(from)) => modelfile.add_from(from.trim()), 350 348 ("parameter", Output::Pair((param, argument))) => { 351 349 modelfile.add_parameter(param, argument.trim())
+2 -1
tiles/Cargo.toml
··· 15 15 futures-util = "0.3" 16 16 hf-hub = {version = "0.4", features = ["tokio"]} 17 17 rustyline = "17.0" 18 - 18 + toml = "1.0.3" 19 19 [dev-dependencies] 20 20 tempfile = "3" 21 + keyring = { version = "3", features = ["apple-native"] }
+58 -1
tiles/src/commands/mod.rs
··· 1 1 // Module that handles CLI commands 2 2 3 + use anyhow::Result; 3 4 use owo_colors::OwoColorize; 4 5 use tiles::runtime::Runtime; 5 - use tiles::utils::config::set_memory_path; 6 + use tiles::utils::accounts::{ 7 + create_root_account, get_root_user_details, save_root_account, set_nickname, 8 + }; 9 + use tiles::utils::config::{get_or_create_config, set_memory_path}; 6 10 use tiles::{core::health, runtime::RunArgs}; 7 11 8 12 pub use tilekit::optimize::optimize; 13 + 14 + use crate::{AccountArgs, AccountCommands}; 9 15 10 16 pub async fn run(runtime: &Runtime, run_args: RunArgs) { 11 17 let _ = runtime.run(run_args).await; ··· 33 39 pub async fn stop_server(runtime: &Runtime) { 34 40 let _ = runtime.stop_server_daemon().await; 35 41 } 42 + 43 + //TODO: add docs 44 + pub fn run_account_commands(account_args: AccountArgs) -> Result<()> { 45 + let config = get_or_create_config()?; 46 + let root_user_details = get_root_user_details(&config)?; 47 + match account_args.command { 48 + Some(AccountCommands::Create { nickname }) => { 49 + //TODO: could show a message if did is already there 50 + let root_user_config = create_root_account(&config, nickname)?; 51 + let id = root_user_config.get("id").unwrap().as_str().unwrap(); 52 + save_root_account(config, &root_user_config)?; 53 + //TODO: color it... 54 + let _ = println!("Root account has been created with id: {}", id).green(); 55 + } 56 + Some(AccountCommands::SetNickname { nickname }) => { 57 + //FIXME: redundant code 58 + if root_user_details.get("id").unwrap().is_empty() { 59 + println!( 60 + "Root account not created yet, use {}", 61 + format!("tiles account create").yellow() 62 + ); 63 + } else { 64 + match set_nickname(&config, nickname) { 65 + Ok(root_user_config) => { 66 + let id = root_user_config.get("id").unwrap().as_str().unwrap(); 67 + let nickname = root_user_config.get("nickname").unwrap().as_str().unwrap(); 68 + save_root_account(config, &root_user_config)?; 69 + println!("Nickname {} has been set for ID: {}", nickname, id) 70 + } 71 + Err(err) => { 72 + println!("Failed to set nickname due to {}", err) 73 + } 74 + } 75 + } 76 + } 77 + _ => { 78 + if root_user_details.get("id").unwrap().is_empty() { 79 + println!( 80 + "Root account not created yet, use {}", 81 + format!("tiles account create").yellow() 82 + ); 83 + } else { 84 + for elem in root_user_details { 85 + println!("{}: {}", elem.0, elem.1) 86 + } 87 + } 88 + } 89 + } 90 + 91 + Ok(()) 92 + }
+22
tiles/src/main.rs
··· 44 44 #[arg(long, default_value = "openai:gpt-4o-mini")] 45 45 model: String, 46 46 }, 47 + /// Manage user account 48 + Account(AccountArgs), 47 49 } 48 50 49 51 #[derive(Debug, Args)] ··· 90 92 SetPath { path: String }, 91 93 } 92 94 95 + #[derive(Debug, Args)] 96 + #[command(args_conflicts_with_subcommands = true)] 97 + #[command(flatten_help = true)] 98 + struct AccountArgs { 99 + #[command(subcommand)] 100 + command: Option<AccountCommands>, 101 + } 102 + 103 + #[derive(Debug, Subcommand)] 104 + enum AccountCommands { 105 + /// Creates a local root account 106 + Create { nickname: Option<String> }, 107 + 108 + /// Sets nickname to local root account 109 + SetNickname { nickname: String }, 110 + } 111 + 93 112 #[tokio::main(flavor = "current_thread")] 94 113 pub async fn main() -> Result<(), Box<dyn Error>> { 95 114 let cli = Cli::parse(); ··· 125 144 let modelfile = commands::optimize(modelfile_path.clone(), data, model).await?; 126 145 std::fs::write(&modelfile_path, modelfile.to_string())?; 127 146 println!("Successfully updated {}", modelfile_path); 147 + } 148 + Commands::Account(account_args) => { 149 + commands::run_account_commands(account_args)?; 128 150 } 129 151 } 130 152 Ok(())
-1
tiles/src/runtime/mlx.rs
··· 118 118 .spawn() 119 119 .expect("failed to start server"); 120 120 121 - fs::create_dir_all(&config_dir).context("Failed to create config directory")?; 122 121 std::fs::write(pid_file, child.id().to_string()).unwrap(); 123 122 println!("Server started with PID {}", child.id()); 124 123 Ok(())
+197
tiles/src/utils/accounts.rs
··· 1 + // Stuff related to account and identity system 2 + 3 + use std::collections::HashMap; 4 + 5 + use anyhow::Result; 6 + use tilekit::accounts::create_identity; 7 + use toml::Table; 8 + 9 + use crate::utils::config::save_config; 10 + const ROOT_USER_CONFIG_KEY: &str = "root-user"; 11 + 12 + //TODO: add docs 13 + pub fn get_root_user_details(config: &Table) -> Result<HashMap<String, String>> { 14 + Ok(get_root_account(&config)) 15 + } 16 + 17 + fn get_root_account(config: &Table) -> HashMap<String, String> { 18 + let root_user = config.get(ROOT_USER_CONFIG_KEY).unwrap(); 19 + let root_user_table = root_user.as_table().unwrap(); 20 + let mut root_user_map = HashMap::new(); 21 + for ele in root_user_table { 22 + root_user_map.insert(ele.0.to_string(), ele.1.as_str().unwrap().to_owned()); 23 + } 24 + root_user_map 25 + } 26 + 27 + // Lets return main config table only, let the caller do whatever it wants... 28 + // TODO: Needed docs 29 + pub fn create_root_account(config: &Table, nickname: Option<String>) -> Result<Table> { 30 + let root_user = config.get(ROOT_USER_CONFIG_KEY).unwrap(); 31 + let root_user_table = root_user.as_table().unwrap(); 32 + let did = root_user_table.get("id").unwrap().as_str().unwrap(); 33 + if did.is_empty() { 34 + let root_user_config = create_root_user(root_user_table, nickname)?; 35 + Ok(root_user_config) 36 + } else { 37 + Ok(root_user_table.clone()) 38 + } 39 + } 40 + 41 + //TODO: docs 42 + pub fn save_root_account(mut config: Table, root_user_config: &Table) -> Result<()> { 43 + config.insert( 44 + String::from(ROOT_USER_CONFIG_KEY), 45 + toml::Value::Table(root_user_config.clone()), 46 + ); 47 + save_config(&config) 48 + } 49 + 50 + // TODO: add docs 51 + pub fn set_nickname(config: &Table, nickname: String) -> Result<Table> { 52 + let root_user = config.get(ROOT_USER_CONFIG_KEY).unwrap(); 53 + let mut root_user_table = root_user.as_table().unwrap().clone(); 54 + let did = root_user_table.get("id").unwrap().as_str().unwrap(); 55 + if did.is_empty() { 56 + Err(anyhow::anyhow!("No Root user available")) 57 + } else { 58 + root_user_table.insert("id".to_owned(), toml::Value::String(did.to_owned())); 59 + root_user_table.insert("nickname".to_owned(), toml::Value::String(nickname)); 60 + Ok(root_user_table) 61 + } 62 + } 63 + 64 + // TODO: add docs 65 + fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> { 66 + // get root user details 67 + let mut root_user_table = root_user_config.clone(); 68 + match create_identity("tiles") { 69 + Ok(did) => { 70 + root_user_table.insert("id".to_owned(), toml::Value::String(did)); 71 + if nickname.is_some() { 72 + root_user_table.insert( 73 + "nickname".to_owned(), 74 + toml::Value::String(nickname.unwrap()), 75 + ); 76 + } 77 + Ok(root_user_table) 78 + } 79 + Err(err) => Err(err), 80 + } 81 + } 82 + 83 + #[cfg(test)] 84 + 85 + mod tests { 86 + use keyring::{mock, set_default_credential_builder}; 87 + use toml::Table; 88 + 89 + use crate::utils::accounts::{create_root_account, get_root_account}; 90 + 91 + #[test] 92 + fn test_get_root_user_details_empty_id() { 93 + let config: Table = toml::from_str( 94 + r#" 95 + [root-user] 96 + id = '' 97 + nickname = '' 98 + "#, 99 + ) 100 + .unwrap(); 101 + let acc_details = get_root_account(&config); 102 + assert!(acc_details.get("id").unwrap().is_empty()); 103 + } 104 + 105 + #[test] 106 + fn test_get_root_user_details_valid_id() { 107 + let config: Table = toml::from_str( 108 + r#" 109 + [root-user] 110 + id = 'did:key:xyz' 111 + nickname = '' 112 + "#, 113 + ) 114 + .unwrap(); 115 + let acc_details = get_root_account(&config); 116 + assert!(acc_details.get("id").unwrap().contains("did:key")); 117 + } 118 + 119 + #[test] 120 + fn test_create_root_account_but_exists() { 121 + let config: Table = toml::from_str( 122 + r#" 123 + [root-user] 124 + id = 'did:key:xyz' 125 + nickname = '' 126 + "#, 127 + ) 128 + .unwrap(); 129 + let root_user = create_root_account(&config, None).unwrap(); 130 + 131 + assert_eq!( 132 + root_user.get("id").unwrap().as_str().unwrap(), 133 + "did:key:xyz" 134 + ); 135 + } 136 + 137 + #[test] 138 + fn test_create_root_account_new() { 139 + set_default_credential_builder(mock::default_credential_builder()); 140 + let config: Table = toml::from_str( 141 + r#" 142 + [root-user] 143 + id = '' 144 + nickname = '' 145 + "#, 146 + ) 147 + .unwrap(); 148 + let root_user = create_root_account(&config, None).unwrap(); 149 + 150 + assert_ne!( 151 + root_user.get("id").unwrap().as_str().unwrap(), 152 + "did:key:xyz" 153 + ); 154 + 155 + assert!( 156 + root_user 157 + .get("id") 158 + .unwrap() 159 + .as_str() 160 + .unwrap() 161 + .starts_with("did:key") 162 + ); 163 + } 164 + 165 + #[test] 166 + fn test_create_root_account_new_w_nickname() { 167 + set_default_credential_builder(mock::default_credential_builder()); 168 + let config: Table = toml::from_str( 169 + r#" 170 + [root-user] 171 + id = '' 172 + nickname = '' 173 + "#, 174 + ) 175 + .unwrap(); 176 + let root_user = create_root_account(&config, Some(String::from("madclaws"))).unwrap(); 177 + 178 + assert_ne!( 179 + root_user.get("id").unwrap().as_str().unwrap(), 180 + "did:key:xyz" 181 + ); 182 + 183 + assert!( 184 + root_user 185 + .get("id") 186 + .unwrap() 187 + .as_str() 188 + .unwrap() 189 + .starts_with("did:key") 190 + ); 191 + 192 + assert_eq!( 193 + root_user.get("nickname").unwrap().as_str().unwrap(), 194 + "madclaws" 195 + ); 196 + } 197 + }
+46
tiles/src/utils/config.rs
··· 1 1 // Configuration related stuff 2 2 3 3 use anyhow::{Context, Result}; 4 + use std::fs::File; 4 5 use std::path::PathBuf; 5 6 use std::str::FromStr; 6 7 use std::{env, fs}; 8 + use toml::Table; 7 9 8 10 pub trait ConfigProvider { 9 11 fn get_config_dir(&self) -> Result<PathBuf>; ··· 119 121 path_buf.to_str().unwrap() 120 122 )) 121 123 } 124 + 125 + //TODO: Add memory path also to config.toml 126 + pub fn get_or_create_config() -> Result<Table> { 127 + let tiles_config_dir = DefaultProvider.get_config_dir()?; 128 + let config_toml_path = tiles_config_dir.join("config.toml"); 129 + 130 + fs::create_dir_all(&tiles_config_dir).context("Failed to create config directory")?; 131 + if config_toml_path.try_exists()? { 132 + let config_str = fs::read_to_string(config_toml_path)?; 133 + Ok(config_str.parse::<Table>()?) 134 + } else { 135 + let init_table: Table = toml::from_str( 136 + r#" 137 + [root-user] 138 + id = '' 139 + nickname = '' 140 + "#, 141 + )?; 142 + fs::write(config_toml_path, init_table.to_string())?; 143 + Ok(init_table) 144 + } 145 + } 146 + 147 + pub fn save_config(config: &Table) -> Result<()> { 148 + let tiles_config_dir = DefaultProvider.get_config_dir()?; 149 + let config_toml_path = tiles_config_dir.join("config.toml"); 150 + 151 + fs::write(config_toml_path, config.to_string())?; 152 + Ok(()) 153 + } 154 + 155 + // pub fn save_config(config: &Table) 156 + //TODO: Add more tests for config.toml 157 + #[cfg(test)] 158 + mod tests { 159 + 160 + use super::*; 161 + 162 + #[test] 163 + fn test_create_config_file() -> Result<()> { 164 + let _config_table = get_or_create_config()?; 165 + Ok(()) 166 + } 167 + }
+1
tiles/src/utils/mod.rs
··· 1 + pub mod accounts; 1 2 pub mod config; 2 3 pub mod hf_model_downloader;
+1
tiles/tests/config.rs
··· 63 63 // assert!(default.ends_with("data/memory") || default.ends_with("data\\memory")); 64 64 // Ok(()) 65 65 // } 66 +