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.
672 lines
23 KiB
Rust
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(())
|
|
} |