//! GyanDoot v2.0 — AI Agent Platform CLI Entry Point //! //! This binary provides the command-line interface for configuring, running, //! and managing GyanDoot agents across multiple platforms. use std::path::PathBuf; use clap::{Parser, Subcommand}; use tracing_subscriber::EnvFilter; /// GyanDoot v2.0 — AI Agent Platform #[derive(Parser, Debug)] #[command(name = "gyandoot", version, about, long_about = None)] struct Cli { /// Path to the config file (default: ~/.config/gyandoot/config.toml) #[arg(short = 'c', long = "config", env = "GYANDOOT_CONFIG")] config: Option, /// Profile name to use #[arg(short = 'P', long = "profile", default_value = "default")] profile: String, /// Log level filter (e.g., info, debug, gyandoot=debug) #[arg(short = 'l', long = "log", default_value = "info", env = "GYANDOOT_LOG")] log_filter: String, #[command(subcommand)] command: Option, } #[derive(Subcommand, Debug)] enum Commands { /// Run the agent interactively Run { /// Platform to run on (cli, telegram, discord) #[arg(default_value = "cli")] platform: String, /// Initial prompt or task description prompt: Option, }, /// Start the gateway server (handles multi-platform sessions) Serve { /// Host to bind the gateway server #[arg(long, default_value = "127.0.0.1")] host: String, /// Port for the gateway server #[arg(long, default_value_t = 8777)] port: u16, }, /// Initialize a default config file Init, /// List available profiles Profiles, /// Manage skills #[command(name = "skills")] Skills { #[command(subcommand)] action: SkillsAction, }, /// Manage memory (MemPalace) #[command(name = "memory")] Memory { #[command(subcommand)] action: MemoryAction, }, /// Browser commands (camofox anti-detection browser) #[command(name = "browser")] Browser { #[command(subcommand)] action: BrowserAction, }, /// Run the interactive setup wizard for provider configuration #[command(name = "setup")] Setup, /// Launch the TUI interface #[command(name = "tui")] Tui, /// Manage GyanDoot system service (install, remove, status) #[command(name = "service")] Service { #[command(subcommand)] action: ServiceAction, }, } #[derive(Subcommand, Debug)] enum SkillsAction { /// List installed skills List, /// Install a skill from path or registry Install { source: String }, /// Remove a skill Remove { name: String }, } #[derive(Subcommand, Debug)] enum MemoryAction { /// Show memory status Status, /// Query memory for content Query { query: String }, /// Export memory to a file Export { path: PathBuf }, } #[derive(Subcommand, Debug)] enum BrowserAction { /// Check camofox-browser health Health, /// Search the web using anti-detection browser Search { /// Search query query: String, /// Site to search (google, youtube, reddit, wikipedia, etc.) #[arg(short = 's', long = "site", default_value = "google")] site: String, }, /// Browse a URL using anti-detection browser Browse { /// URL to visit url: String, }, } #[derive(Subcommand, Debug)] enum ServiceAction { /// Install GyanDoot as a system service (auto-start on boot) Install, /// Remove the GyanDoot system service Remove, /// Check GyanDoot service status Status, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); // Initialize tracing subscriber tracing_subscriber::fmt() .with_env_filter(EnvFilter::new(&cli.log_filter)) .with_target(true) .init(); tracing::debug!("GyanDoot v2.0 starting (profile: {})", cli.profile); tracing::debug!("Config path: {:?}", cli.config); // Resolve config dir let _config_dir = cli.config.unwrap_or_else(default_config_dir); tracing::debug!("Using config dir: {}", _config_dir.display()); match cli.command { Some(Commands::Run { platform, prompt }) => { cmd_run(&cli.profile, &platform, prompt.as_deref()).await?; } Some(Commands::Serve { host, port }) => { cmd_serve(&cli.profile, &host, port).await?; } Some(Commands::Init) => { cmd_init().await?; } Some(Commands::Profiles) => { cmd_profiles().await?; } Some(Commands::Skills { action }) => { cmd_skills(action).await?; } Some(Commands::Memory { action }) => { cmd_memory(action).await?; } Some(Commands::Browser { action }) => { cmd_browser(action).await?; } Some(Commands::Setup) => { cmd_setup().await?; } Some(Commands::Tui) => { cmd_tui().await?; } Some(Commands::Service { action }) => { cmd_service(action).await?; } None => { // Default: run interactive CLI mode cmd_run(&cli.profile, "cli", None).await?; } } Ok(()) } /// Returns the default config directory: ~/.config/gyandoot/ fn default_config_dir() -> PathBuf { let home = dirs::home_dir().expect("Cannot determine home directory"); home.join(".config").join("gyandoot") } /// Run the agent in the given platform mode async fn cmd_run(profile_name: &str, platform: &str, prompt: Option<&str>) -> anyhow::Result<()> { // Print the banner on launch gyandoot_tui::banner::print_banner(); tracing::info!("Running agent (profile={}, platform={})", profile_name, platform); // Load config from user config files, falling back to defaults let config = load_agent_config(); let agent = gyandoot_agent::agent_loop::AgentLoop::new(config); if let Some(p) = prompt { tracing::info!("Starting with prompt: {}", p); } agent.run().await?; Ok(()) } /// Load agent config from user config files with fallback to defaults. /// /// Resolution order: /// 1. `~/.config/gyandoot/config.toml` (primary — set up via `gyandoot setup` or `gyandoot init`) /// 2. `~/.config/gyandoot/config.toml` via XDG (macOS: ~/Library/Application Support/) /// 3. `~/.gyandoot/config.yaml` (legacy) /// 4. `AgentConfig::default()` (hardcoded defaults) fn load_agent_config() -> gyandoot_core::config::AgentConfig { // 1. Try ~/.config/gyandoot/config.toml (explicit — works on all platforms) if let Some(home) = dirs::home_dir() { let explicit_path = home.join(".config").join("gyandoot").join("config.toml"); if explicit_path.exists() { match load_and_convert_config(&explicit_path) { Ok(cfg) => return cfg, Err(e) => tracing::warn!("Failed to parse {}: {}", explicit_path.display(), e), } } } // 2. Try XDG-style config (macOS: ~/Library/Application Support/) if let Some(cfg_dir) = dirs::config_dir() { let xdg_path = cfg_dir.join("gyandoot").join("config.toml"); if xdg_path.exists() { match load_and_convert_config(&xdg_path) { Ok(cfg) => return cfg, Err(e) => tracing::warn!("Failed to parse {}: {}", xdg_path.display(), e), } } } // 3. Try legacy config: ~/.gyandoot/config.yaml if let Some(home) = dirs::home_dir() { let legacy_path = home.join(".gyandoot").join("config.yaml"); if legacy_path.exists() { match std::fs::read_to_string(&legacy_path) { Ok(contents) => { match serde_yaml::from_str::(&contents) { Ok(gyandoot_cfg) => { tracing::info!("Loaded config from {}", legacy_path.display()); return gyandoot_core::config::AgentConfig::from(gyandoot_cfg); } Err(e) => tracing::warn!("Failed to parse {}: {}", legacy_path.display(), e), } } Err(e) => tracing::warn!("Failed to read {}: {}", legacy_path.display(), e), } } } // 4. Fallback to hardcoded defaults tracing::warn!("No config file found; using default configuration (API keys will be empty)"); gyandoot_core::config::AgentConfig::default() } /// Load a TOML config file, converting `[provider]` (singular, flat format /// written by `gyandoot setup`) to the canonical `[providers]` format. fn load_and_convert_config(path: &std::path::Path) -> Result> { let content = std::fs::read_to_string(path)?; // Detect format: flat `[provider]` (singular, from gyandoot setup) or // canonical `[providers]` (plural). We check the raw content because // GyandootConfig has #[serde(default)] on everything — silent deser // swallows unknown sections and returns defaults instead of erroring. let is_flat_format = content.contains("[provider]\n") || content.contains("[provider]") && !content.contains("[providers]"); if !is_flat_format { // Canonical format — parse directly let gyandoot_cfg = toml::from_str::(&content)?; tracing::info!("Loaded config from {}", path.display()); return Ok(gyandoot_core::config::AgentConfig::from(gyandoot_cfg)); } // Flat `[provider]` format — parse and convert to canonical #[derive(serde::Deserialize)] struct FlatConfig { provider: FlatProvider, agent: Option, } #[derive(serde::Deserialize)] struct FlatProvider { kind: String, base_url: String, api_key: Option, model: String, temperature: Option, max_tokens: Option, #[allow(dead_code)] #[serde(default)] merge_system: bool, #[allow(dead_code)] #[serde(default)] no_tools: bool, } #[derive(serde::Deserialize)] struct FlatAgent { name: Option, } let flat: FlatConfig = toml::from_str(&content)?; tracing::info!("Loaded config from {} (converted from flat [provider] format)", path.display()); // Build canonical GyandootConfig from flat data let provider_name = flat.provider.kind.to_lowercase(); let mut providers_map = std::collections::HashMap::new(); let mut provider_table = toml::map::Map::new(); provider_table.insert("model".to_string(), toml::Value::String(flat.provider.model.clone())); provider_table.insert("base_url".to_string(), toml::Value::String(flat.provider.base_url)); if let Some(key) = &flat.provider.api_key { provider_table.insert("api_key".to_string(), toml::Value::String(key.clone())); } providers_map.insert(provider_name.clone(), toml::Value::Table(provider_table)); let gyandoot_cfg = gyandoot_core::config::GyandootConfig { agent: gyandoot_core::config::AgentSection { name: flat.agent.and_then(|a| a.name).unwrap_or_else(|| "gyandoot".to_string()), model: flat.provider.model.clone(), max_tokens: flat.provider.max_tokens.unwrap_or(4096) as usize, temperature: flat.provider.temperature.unwrap_or(0.7), system_prompt: String::new(), }, providers: gyandoot_core::config::ProvidersSection { default: provider_name, providers: providers_map, }, identity: gyandoot_core::config::IdentitySection::default(), platform: gyandoot_core::config::PlatformSection::default(), memory: gyandoot_core::config::MemorySection::default(), gateway: gyandoot_core::config::GatewaySection::default(), browser: gyandoot_core::config::BrowserSection::default(), }; Ok(gyandoot_core::config::AgentConfig::from(gyandoot_cfg)) } /// Start the gateway server for multi-platform sessions async fn cmd_serve(profile_name: &str, host: &str, port: u16) -> anyhow::Result<()> { tracing::info!( "Starting gateway server (profile={}, bind={}:{})", profile_name, host, port ); let gateway = gyandoot_gateway::GatewayRunner::new(); gateway.start().await?; tracing::info!("Gateway running on {}:{} — press Ctrl+C to stop", host, port); // Keep running until signal tokio::signal::ctrl_c().await?; gateway.stop().await; Ok(()) } /// Initialize default config files async fn cmd_init() -> anyhow::Result<()> { let config_dir = default_config_dir(); tracing::info!("Initializing config in {}", config_dir.display()); std::fs::create_dir_all(&config_dir)?; let config_path = config_dir.join("config.toml"); if config_path.exists() { tracing::warn!("Config already exists at {}", config_path.display()); return Ok(()); } let default_config = r#" [agent] name = "gyandoot" model = "gpt-4o" max_tokens = 4096 temperature = 0.7 system_prompt = "You are GyanDoot, an AI Agent Platform." [providers] default = "openai" [providers.openai] api_key = "sk-your-api-key-here" model = "gpt-4o" [platform] default = "cli" [memory] backend = "local" "#; std::fs::write(&config_path, default_config.trim())?; tracing::info!("Created default config at {}", config_path.display()); Ok(()) } /// List available profiles async fn cmd_profiles() -> anyhow::Result<()> { let resolver = gyandoot_profile::ConfigResolver::default(); let profiles_dir = resolver.profiles_dir(); if profiles_dir.exists() { let mut entries: Vec<_> = std::fs::read_dir(&profiles_dir)? .filter_map(|e| e.ok()) .filter(|e| e.path().is_dir()) .map(|e| e.file_name().to_string_lossy().to_string()) .collect(); entries.sort(); if entries.is_empty() { println!("No profiles found. Use 'gyandoot init' to create one."); } else { println!("Available profiles:"); for p in &entries { println!(" - {}", p); } } } else { println!("No profiles found. Use 'gyandoot init' to create one."); } Ok(()) } /// Handle skill management subcommands async fn cmd_skills(action: SkillsAction) -> anyhow::Result<()> { match action { SkillsAction::List => { let mut manager = gyandoot_skills::SkillsManager::new(); let skills = manager.scan(); if skills.is_empty() { println!("No skills installed."); } else { println!("Installed skills:"); for s in &skills { println!(" - {} ({:?})", s.name, s.trust); } } } SkillsAction::Install { source } => { tracing::info!("Skill install from {} (placeholder — integrate skill hub)", source); } SkillsAction::Remove { name } => { tracing::info!("Skill remove: {} (placeholder)", name); } } Ok(()) } /// Handle memory management subcommands async fn cmd_memory(action: MemoryAction) -> anyhow::Result<()> { let palace = gyandoot_memory::palace::MemPalace::new("default"); match action { MemoryAction::Status => { #[allow(unused_variables)] let status_msg = format!("Memory: initialized ({})", "in-memory"); println!("{}", status_msg); } MemoryAction::Query { query } => { println!("Memory query for: {}", query); #[allow(unused_variables)] let _results = palace.query(&query, 10); println!(" (full memory query not yet implemented in CLI)"); } MemoryAction::Export { path } => { tracing::info!("Exporting memory to {}", path.display()); } } Ok(()) } /// Handle browser commands (camofox anti-detection browser) async fn cmd_browser(action: BrowserAction) -> anyhow::Result<()> { use gyandoot_browser::BrowserClient; use gyandoot_agent::browser_tools::{WebSearchTool, WebBrowseTool}; use gyandoot_agent::tools::Tool; let client = BrowserClient::new(); match action { BrowserAction::Health => { match client.health().await { Ok(health) => { println!("✅ Camofox Browser — Health OK"); println!(" Engine: {}", health.engine); println!(" Browser connected: {}", health.browser_connected); println!(" Active tabs: {}", health.active_tabs); println!(" Active sessions: {}", health.active_sessions); } Err(e) => { eprintln!("❌ Camofox not reachable: {}", e); eprintln!(" Start with: cd ~/camofox-browser && npm start"); } } } BrowserAction::Search { query, site } => { let tool = WebSearchTool::new(client); let args = serde_json::json!({"query": query, "site": site}); match tool.execute(args).await { Ok(result) => println!("{}", result), Err(e) => eprintln!("Search failed: {}", e), } } BrowserAction::Browse { url } => { let tool = WebBrowseTool::new(client); let args = serde_json::json!({"url": url}); match tool.execute(args).await { Ok(result) => println!("{}", result), Err(e) => eprintln!("Browse failed: {}", e), } } } Ok(()) } /// Run the interactive setup wizard async fn cmd_setup() -> anyhow::Result<()> { tracing::info!("Starting setup wizard"); match gyandoot_tui::setup::run_setup_wizard() { Ok(Some(result)) => { if result.connection_ok { println!("\n✅ Setup complete! Provider '{}' configured successfully.", result.provider); } else { println!("\n⚠️ Setup saved but connection test failed for '{}'.", result.provider); println!(" Check your API key and base URL, then try again."); } println!(" Config: {}", result.config_path.display()); } Ok(None) => { println!("Setup cancelled."); } Err(e) => { tracing::error!("Setup wizard failed: {}", e); eprintln!("Setup error: {}", e); } } Ok(()) } /// Launch the TUI interface with a live agent loop async fn cmd_tui() -> anyhow::Result<()> { tracing::info!("Launching TUI with live agent"); // Load agent config let config = load_agent_config(); // Create channels for TUI ↔ agent communication let (user_tx, mut user_rx) = tokio::sync::mpsc::channel::(256); let (event_tx, event_rx) = tokio::sync::mpsc::channel::(256); // Spawn agent task let agent = gyandoot_agent::agent_loop::AgentLoop::new(config); let agent_event_tx = event_tx.clone(); tokio::spawn(async move { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status("INITIALISING...".to_string())).await; // Init session (browser, identity, memory — like interactive run()) if let Err(e) = agent.init_session().await { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status( format!("SESSION INIT ERROR: {}", e) )).await; } else { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status("AGENT READY".to_string())).await; } // Send initial telemetry let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Telemetry { provider: agent.config().provider.default.clone(), model: agent.config().model.clone(), total_tokens: 0, max_tokens: agent.config().max_tokens, msg_count: 0, }).await; // Process messages from TUI while let Some(user_msg) = user_rx.recv().await { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status("PROCESSING...".to_string())).await; match agent.session_turn(&user_msg).await { Ok(text) => { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Telemetry { provider: agent.config().provider.default.clone(), model: agent.config().model.clone(), total_tokens: 0, max_tokens: agent.config().max_tokens, msg_count: 0, }).await; let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Response(text)).await; } Err(e) => { let _ = agent_event_tx.send(gyandoot_tui::tui::TuiEvent::Status( format!("ERROR: {}", e) )).await; } } } }); // Run TUI (this blocks until user quits) gyandoot_tui::tui::run_tui(event_rx, user_tx).await?; tracing::info!("TUI closed"); Ok(()) } /// Handle service management subcommands async fn cmd_service(action: ServiceAction) -> anyhow::Result<()> { use gyandoot_core::service; match action { ServiceAction::Install => { println!("📦 Installing GyanDoot system service..."); match service::install_service() { Ok(status) => { println!("✅ GyanDoot service installed successfully!"); println!(" Init system: {}", status.init_system); println!(" Status: {}", status.details); println!("\n GyanDoot will now start automatically on boot."); } Err(e) => { eprintln!("❌ Failed to install service: {}", e); eprintln!(" You can still run: gyandoot run"); } } } ServiceAction::Remove => { println!("🗑️ Removing GyanDoot system service..."); match service::remove_service() { Ok(()) => { println!("✅ GyanDoot service removed."); } Err(e) => { eprintln!("❌ Failed to remove service: {}", e); } } } ServiceAction::Status => { match service::check_service() { Ok(status) => { println!("📊 GyanDoot Service Status"); println!(" Name: {}", status.name); println!(" Init System: {}", status.init_system); println!(" Installed: {}", if status.installed { "✅ Yes" } else { "❌ No" }); println!(" Running: {}", if status.running { "✅ Yes" } else { "❌ No" }); println!(" Auto-start: {}", if status.enabled { "✅ Enabled" } else { "❌ Disabled" }); println!(" Details: {}", status.details); if !status.installed { println!("\n 💡 Install with: gyandoot service install"); } } Err(e) => { eprintln!("❌ Failed to check service status: {}", e); } } } } Ok(()) }