Files
gyandoot-v2/src/main.rs
Tara Menon 3030f6eadf fix(tui): stateful session with identity, memory, banner
Two fixes for the TUI:

1. **Raw LLM responses** —  was stateless, creating
   fresh messages every turn. Added  +
   methods that accumulate conversation history, inject identity, load
   past memory, run browser init, and compress context — identical to
   the interactive  loop but designed for external callers.

2. **Banner flash** — removed  call that fired to stdout
   *before* TUI entered alternate screen. TUI already shows the robot
   head via  as initial chat content.

Also added  getter and  field to AgentLoop.
2026-06-12 13:00:20 +05:30

672 lines
23 KiB
Rust

//! 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<PathBuf>,
/// 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<Commands>,
}
#[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<String>,
},
/// 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::<gyandoot_core::config::GyandootConfig>(&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<gyandoot_core::config::AgentConfig, Box<dyn std::error::Error>> {
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::<gyandoot_core::config::GyandootConfig>(&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<FlatAgent>,
}
#[derive(serde::Deserialize)]
struct FlatProvider {
kind: String,
base_url: String,
api_key: Option<String>,
model: String,
temperature: Option<f64>,
max_tokens: Option<u32>,
#[allow(dead_code)]
#[serde(default)]
merge_system: bool,
#[allow(dead_code)]
#[serde(default)]
no_tools: bool,
}
#[derive(serde::Deserialize)]
struct FlatAgent {
name: Option<String>,
}
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::<String>(256);
let (event_tx, event_rx) = tokio::sync::mpsc::channel::<gyandoot_tui::tui::TuiEvent>(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(())
}