New command: 'gyandoot platform configure' Walks users through: 1. Telegram bot token input (from @BotFather) 2. Discord bot token + guild ID input 3. Summary showing which platforms are configured 4. Saves [platform] section to config.toml Existing 'gyandoot setup' and flat config parser also handle the new [platform] section format.
756 lines
26 KiB
Rust
756 lines
26 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,
|
|
},
|
|
/// Configure messaging platforms (Telegram, Discord)
|
|
#[command(name = "platform")]
|
|
Platform {
|
|
#[command(subcommand)]
|
|
action: PlatformAction,
|
|
},
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum PlatformAction {
|
|
/// Interactive wizard to configure Telegram and Discord bots
|
|
#[command(name = "configure")]
|
|
Configure,
|
|
}
|
|
|
|
#[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?;
|
|
}
|
|
Some(Commands::Platform { action }) => {
|
|
cmd_platform(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>,
|
|
#[serde(default)]
|
|
browser: Option<FlatBrowser>,
|
|
}
|
|
|
|
#[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>,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct FlatBrowser {
|
|
#[serde(default)]
|
|
node_path: 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 {
|
|
node_path: flat.browser.and_then(|b| b.node_path),
|
|
..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={})", profile_name);
|
|
|
|
let config = load_agent_config();
|
|
let agent = gyandoot_agent::agent_loop::AgentLoop::new(config.clone());
|
|
|
|
// Build platform adapters from config
|
|
let mut platforms: Vec<Box<dyn gyandoot_platform::Platform>> = Vec::new();
|
|
|
|
// Telegram
|
|
if let Some(tg) = &config.platform.telegram {
|
|
if !tg.bot_token.is_empty() {
|
|
platforms.push(Box::new(
|
|
gyandoot_platform::telegram::TelegramPlatform::new(&tg.bot_token),
|
|
));
|
|
tracing::info!("Telegram platform added");
|
|
}
|
|
}
|
|
|
|
// Discord
|
|
if let Some(dc) = &config.platform.discord {
|
|
if !dc.bot_token.is_empty() {
|
|
platforms.push(Box::new(
|
|
gyandoot_platform::discord::DiscordPlatform::new(&dc.bot_token, dc.guild_id),
|
|
));
|
|
tracing::info!("Discord platform added");
|
|
}
|
|
}
|
|
|
|
// Gateway
|
|
let mut gateway = gyandoot_gateway::GatewayRunner::new(platforms);
|
|
gateway.start().await?;
|
|
tracing::info!("Gateway started — listening on platforms");
|
|
|
|
tokio::select! {
|
|
_ = tokio::signal::ctrl_c() => {
|
|
tracing::info!("Shutting down gateway...");
|
|
gateway.stop();
|
|
}
|
|
result = gateway.serve(&agent) => {
|
|
match result {
|
|
Ok(()) => tracing::info!("Gateway serve completed"),
|
|
Err(e) => tracing::error!("Gateway serve error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
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(())
|
|
}
|
|
/// Configure messaging platforms (Telegram, Discord)
|
|
async fn cmd_platform(action: PlatformAction) -> anyhow::Result<()> {
|
|
match action {
|
|
PlatformAction::Configure => {
|
|
tracing::info!("Starting platform configuration wizard");
|
|
match gyandoot_tui::platform::run_platform_wizard() {
|
|
Ok(Some(_config)) => {
|
|
println!("✅ Platform configuration saved!");
|
|
println!(" Run 'gyandoot serve' to start the gateway.");
|
|
}
|
|
Ok(None) => {
|
|
println!("Platform configuration cancelled.");
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Platform wizard failed: {}", e);
|
|
eprintln!("Platform setup error: {}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|