use anyhow::Result;
use clap::{Parser, ValueEnum};
use log::{error, info, warn, debug};
use std::sync::{Arc, Mutex};
use tokio::sync::oneshot;
use tokio::time::Duration;
use uuid::Uuid;
use walkdir::WalkDir;
use agentic_loop::AgenticLoop;
use artifact::ArtifactManager;
use config::Config;
use context::{ContextConfig, ContextManager};
use event_bus::{Event, EventBus, EventEmitter};
use llm_manager::{LLMManager, LLMProvider, LocalProvider};
use providers::{
anthropic::AnthropicProvider, ollama::OllamaProvider, openai::OpenAIProvider, openrouter::OpenRouterProvider, gemini::GeminiProvider, xai::XAIProvider,
};
use ui_dashboard::DashboardUI;
use ui_enhanced::EnhancedUI;
use tool_manager::ToolManager;
mod logger_dashboard;
mod agentic_loop;
mod artifact;
mod command_executor;
mod concurrency;
mod config;
mod context;
mod event_bus;
mod executor;
mod interpreter;
mod iteration_context;
mod llm_manager;
mod logger;
mod planner;
mod providers;
mod reviewer;
mod mcp;
mod tool_manager;
mod ui_dashboard;
mod ui_enhanced;
#[derive(ValueEnum, Debug, Clone)]
enum CommandKind {
#[clap(help = "Code generation")]
Code,
#[clap(help = "Refactoring")]
Refactor,
#[clap(help = "Code review")]
Review,
#[clap(help = "Documentation generation")]
Docs,
#[clap(help = "Security analysis")]
Security,
}
#[derive(Parser, Debug)]
#[command(
name = "cli_engineer",
about = "Agentic CLI for software engineering automation",
version = env!("CARGO_PKG_VERSION")
)]
struct Args {
#[arg(short, long)]
verbose: bool,
#[arg(long)]
no_dashboard: bool,
#[arg(short, long)]
config: Option<String>,
#[arg(value_enum)]
command: CommandKind,
#[arg(last = true)]
prompt: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
let args = Args::parse();
let event_bus = Arc::new(EventBus::new(1000));
if !args.no_dashboard {
let level = if args.verbose {
log::LevelFilter::Info
} else {
log::LevelFilter::Warn
};
logger_dashboard::DashboardLogger::init_with_file(event_bus.clone(), level, args.verbose)
.expect("Failed to init DashboardLogger");
} else {
if args.verbose {
logger::init_with_file_logging(args.verbose);
} else {
logger::init(args.verbose);
}
}
let config = Arc::new(Config::load(&args.config)?);
let prompt = args.prompt.join(" ");
if !args.no_dashboard {
let mut ui = DashboardUI::new(false);
ui.set_event_bus(event_bus.clone());
ui.start()?;
if matches!(args.command, CommandKind::Code) && prompt.is_empty() {
ui.display_error("PROMPT required for code command")?;
ui.finish()?;
return Ok(());
}
let ui_ref = Arc::new(Mutex::new(ui));
let ui_clone = ui_ref.clone();
let (stop_tx, mut stop_rx) = oneshot::channel();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_millis(100));
loop {
tokio::select! {
_ = interval.tick() => {
if let Ok(mut ui_guard) = ui_clone.try_lock() {
let _ = ui_guard.throttled_render();
}
}
_ = &mut stop_rx => break,
}
}
});
let result = match args.command {
CommandKind::Code => run_with_ui(prompt.clone(), config.clone(), event_bus.clone(), true, args.command).await,
CommandKind::Refactor => {
let p = if prompt.is_empty() {
"Analyze the current directory and perform recommended refactoring.".to_string()
} else {
prompt.clone()
};
run_with_ui(
format!("Refactor codebase. {}", p),
config.clone(),
event_bus.clone(),
true,
args.command,
)
.await
}
CommandKind::Review => {
let p = if prompt.is_empty() {
"ANALYSIS ONLY: Review the codebase files and create a comprehensive code review report. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your findings, suggestions, and recommendations in code_review.md. Focus on code quality, best practices, potential issues, and improvement opportunities.".to_string()
} else {
format!("ANALYSIS ONLY: Review the codebase with focus on: {}. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your findings in code_review.md", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
CommandKind::Docs => {
let p = if prompt.is_empty() {
"Generate comprehensive documentation for the codebase. Create documentation files in a docs/ directory.".to_string()
} else {
format!("Generate documentation for the codebase with these instructions: {}. Create documentation files in a docs/ directory.", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
CommandKind::Security => {
let p = if prompt.is_empty() {
"SECURITY ANALYSIS ONLY: Perform a comprehensive security analysis of the codebase. DO NOT generate, modify, or create any source code files. ONLY analyze existing code for vulnerabilities, security issues, and best practice violations. Document your findings, risk assessments, and security recommendations in security_report.md.".to_string()
} else {
format!("SECURITY ANALYSIS ONLY: Perform a security analysis of the codebase focusing on: {}. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your security findings in security_report.md", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
};
match result {
Ok(_) => {
let _ = stop_tx.send(());
let _ = handle.await;
if let Ok(mut ui_guard) = ui_ref.try_lock() {
ui_guard.finish()?;
}
}
Err(e) => {
let _ = stop_tx.send(());
let _ = handle.await;
if let Ok(mut ui_guard) = ui_ref.try_lock() {
ui_guard.display_error(&format!("{}", e))?;
ui_guard.finish()?;
}
return Err(e);
}
}
} else {
let mut ui = if config.ui.colorful && config.ui.progress_bars && args.verbose {
EnhancedUI::new(false)
} else {
EnhancedUI::new(true) };
ui.set_event_bus(event_bus.clone());
ui.start()?;
if matches!(args.command, CommandKind::Code) && prompt.is_empty() {
ui.display_error("PROMPT required for code command").await?;
ui.finish();
return Ok(());
}
let result = match args.command {
CommandKind::Code => run_with_ui(prompt.clone(), config.clone(), event_bus.clone(), true, args.command).await,
CommandKind::Refactor => {
let p = if prompt.is_empty() {
"Analyze the current directory and perform recommended refactoring.".to_string()
} else {
prompt.clone()
};
run_with_ui(
format!("Refactor codebase. {}", p),
config.clone(),
event_bus.clone(),
true,
args.command,
)
.await
}
CommandKind::Review => {
let p = if prompt.is_empty() {
"ANALYSIS ONLY: Review the codebase files and create a comprehensive code review report. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your findings, suggestions, and recommendations in code_review.md. Focus on code quality, best practices, potential issues, and improvement opportunities.".to_string()
} else {
format!("ANALYSIS ONLY: Review the codebase with focus on: {}. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your findings in code_review.md", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
CommandKind::Docs => {
let p = if prompt.is_empty() {
"Generate comprehensive documentation for the codebase. Create documentation files in a docs/ directory.".to_string()
} else {
format!("Generate documentation for the codebase with these instructions: {}. Create documentation files in a docs/ directory.", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
CommandKind::Security => {
let p = if prompt.is_empty() {
"SECURITY ANALYSIS ONLY: Perform a comprehensive security analysis of the codebase. DO NOT generate, modify, or create any source code files. ONLY analyze existing code for vulnerabilities, security issues, and best practice violations. Document your findings, risk assessments, and security recommendations in security_report.md.".to_string()
} else {
format!("SECURITY ANALYSIS ONLY: Perform a security analysis of the codebase focusing on: {}. DO NOT generate, modify, or create any source code files. ONLY analyze existing code and document your security findings in security_report.md", prompt)
};
run_with_ui(p, config.clone(), event_bus.clone(), true, args.command).await
}
};
match result {
Ok(_) => ui.finish(),
Err(e) => {
ui.display_error(&format!("{}", e)).await?;
ui.finish();
return Err(e);
}
}
}
Ok(())
}
async fn scan_and_populate_context(
context_manager: &ContextManager,
context_id: &str,
event_bus: Arc<EventBus>,
) -> Result<(usize, String)> {
let _ = event_bus
.emit(Event::LogLine {
level: "INFO".to_string(),
message: "Scanning codebase for context...".to_string(),
})
.await;
let mut file_count = 0;
let mut file_list = Vec::new();
let current_dir = std::env::current_dir()?;
let code_extensions = vec![
"rs", "py", "js", "ts", "java", "c", "cpp", "h", "hpp", "go",
"rb", "php", "swift", "kt", "scala", "sh", "bash", "yaml", "yml",
"json", "toml", "xml", "html", "css", "jsx", "tsx", "vue", "svelte", "md"
];
let config_files = vec![
"Cargo.toml", "package.json", "pom.xml", "build.gradle",
"requirements.txt", "setup.py", "Gemfile", "composer.json",
"Makefile", "Dockerfile", ".gitignore", "README.md", "README"
];
for entry in WalkDir::new(¤t_dir)
.max_depth(5)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
!name.starts_with('.') &&
name != "target" &&
name != "node_modules" &&
name != "venv" &&
name != "dist" &&
name != "build"
})
{
let entry = entry?;
let path = entry.path();
if path.is_file() {
let file_name = path.file_name().unwrap().to_string_lossy();
let ext = path.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let should_include = code_extensions.contains(&ext) ||
config_files.iter().any(|&cf| file_name == cf);
if should_include {
let metadata = std::fs::metadata(&path)?;
if metadata.len() > 100_000 {
info!("Skipping large file {:?} ({}KB)", path, metadata.len() / 1024);
continue;
}
match std::fs::read_to_string(&path) {
Ok(content) => {
let relative_path = path.strip_prefix(¤t_dir)
.unwrap_or(path)
.to_string_lossy();
let file_info = format!(
"File: {}\n```{}\n{}\n```",
relative_path,
ext.to_string(),
content
);
context_manager
.add_message(context_id, "system".to_string(), file_info)
.await?;
file_count += 1;
file_list.push(relative_path.to_string());
info!("Added {} to context ({} bytes)", relative_path, content.len());
}
Err(e) => {
warn!("Failed to read {:?}: {}", path, e);
}
}
}
}
}
event_bus
.emit(Event::LogLine {
level: "INFO".to_string(),
message: format!("Scanning complete. Added {} files to context", file_count),
})
.await?;
info!("Scan complete: added {} files to context", file_count);
let file_summary = if file_count > 0 {
format!("\n\nThe following {} files from this codebase have been loaded into context:\n{}",
file_count,
file_list.join("\n"))
} else {
String::new()
};
Ok((file_count, file_summary))
}
async fn run_with_ui(prompt: String, config: Arc<Config>, event_bus: Arc<EventBus>, scan_codebase: bool, command: CommandKind) -> Result<()> {
let (llm_manager, artifact_manager, context_manager, tool_manager) =
setup_managers(&*config, event_bus.clone()).await?;
let task_id = Uuid::new_v4().to_string();
event_bus
.emit(Event::TaskStarted {
task_id: task_id.clone(),
description: prompt.clone(),
})
.await?;
info!("Emitting TaskStarted event for task: {}", prompt);
let agentic_loop = AgenticLoop::new(
llm_manager.clone(),
config.execution.max_iterations,
event_bus.clone(),
)
.with_context_manager(context_manager.clone())
.with_config(config.clone())
.with_artifact_manager(artifact_manager.clone())
.with_tool_manager(tool_manager.clone())
.with_command(command);
info!("AgenticLoop instance created.");
let ctx_id = context_manager
.create_context(std::collections::HashMap::new())
.await;
info!("Context created. Running agentic loop...");
event_bus
.emit(Event::LogLine {
level: "INFO".to_string(),
message: "Execution started".to_string(),
})
.await?;
let mut enhanced_prompt = prompt;
if scan_codebase {
let (file_count, file_summary) = scan_and_populate_context(&context_manager, &ctx_id, event_bus.clone()).await?;
if file_count > 0 {
enhanced_prompt = format!("{}{}", enhanced_prompt, file_summary);
}
}
let result = agentic_loop.run(&enhanced_prompt, &ctx_id).await;
info!("Agentic loop completed");
match result {
Ok(_) => {
info!("Task completed successfully");
event_bus
.emit(Event::TaskCompleted {
task_id: task_id.clone(),
result: "Success".to_string(),
})
.await?;
}
Err(ref e) => {
error!("Task failed: {}", e);
event_bus
.emit(Event::TaskFailed {
task_id,
error: e.to_string(),
})
.await?;
}
}
if config.execution.cleanup_on_exit {
info!("Cleaning up artifacts...");
artifact_manager.cleanup().await?;
}
result.map(|_| ())
}
async fn setup_managers(
config: &Config,
event_bus: Arc<EventBus>,
) -> Result<(Arc<LLMManager>, Arc<ArtifactManager>, Arc<ContextManager>, Arc<ToolManager>)> {
let mut artifact_manager =
ArtifactManager::new(std::env::current_dir()?.join(&config.execution.artifact_dir))?;
artifact_manager.set_event_bus(event_bus.clone());
let artifact_manager = Arc::new(artifact_manager);
let context_config = ContextConfig {
max_tokens: config.context.max_tokens,
compression_threshold: config.context.compression_threshold,
cache_enabled: config.context.cache_enabled,
cache_dir: std::env::current_dir()?
.join(".cli_engineer")
.join("context_cache"),
};
let mut context_manager = ContextManager::new(context_config)?;
context_manager.set_event_bus(event_bus.clone());
let mut providers: Vec<Box<dyn LLMProvider>> = Vec::new();
if let Some(openrouter_config) = &config.ai_providers.openrouter {
if openrouter_config.enabled {
match OpenRouterProvider::new(
Some(openrouter_config.model.clone()),
openrouter_config.temperature,
openrouter_config.max_tokens,
) {
Ok(provider) => {
info!("OpenRouter provider initialized successfully");
providers.push(Box::new(provider));
}
Err(e) => {
warn!("Failed to initialize OpenRouter provider: {}. Skipping.", e);
}
}
}
}
if let Some(gemini_config) = &config.ai_providers.gemini {
if gemini_config.enabled {
match GeminiProvider::new(
Some(gemini_config.model.clone()),
gemini_config.temperature,
gemini_config.cost_per_1m_input_tokens,
gemini_config.cost_per_1m_output_tokens,
Some(event_bus.clone()),
) {
Ok(provider) => {
info!("Gemini provider initialized successfully");
providers.push(Box::new(provider));
}
Err(e) => {
warn!("Failed to initialize Gemini provider: {}. Skipping.", e);
}
}
}
}
if let Some(xai_config) = &config.ai_providers.xai {
if xai_config.enabled {
match XAIProvider::new(
Some(xai_config.model.clone()),
xai_config.temperature,
) {
Ok(provider) => {
info!("xAI provider initialized successfully");
providers.push(Box::new(provider
.with_event_bus(event_bus.clone())
.with_cost_per_1m_input_tokens(xai_config.cost_per_1m_input_tokens.unwrap_or(0.0))
.with_cost_per_1m_output_tokens(xai_config.cost_per_1m_output_tokens.unwrap_or(0.0))));
}
Err(e) => {
warn!("Failed to initialize xAI provider: {}. Skipping.", e);
}
}
}
}
if let Some(openai_config) = &config.ai_providers.openai {
debug!("Found OpenAI config: enabled={}, model={}", openai_config.enabled, openai_config.model);
if openai_config.enabled {
debug!("OpenAI provider is enabled, initializing...");
match OpenAIProvider::new(
Some(openai_config.model.clone()),
openai_config.temperature,
) {
Ok(provider) => {
info!("OpenAI provider initialized successfully");
providers.push(Box::new(provider
.with_event_bus(event_bus.clone())
.with_cost_per_1m_input_tokens(openai_config.cost_per_1m_input_tokens.unwrap_or(0.0))
.with_cost_per_1m_output_tokens(openai_config.cost_per_1m_output_tokens.unwrap_or(0.0))));
}
Err(e) => {
warn!("Failed to initialize OpenAI provider: {}. Skipping.", e);
}
}
} else {
debug!("OpenAI provider is disabled in config");
}
} else {
debug!("No OpenAI config found");
}
if let Some(anthropic_config) = &config.ai_providers.anthropic {
debug!("Found Anthropic config: enabled={}, model={}", anthropic_config.enabled, anthropic_config.model);
if anthropic_config.enabled {
debug!("Anthropic provider is enabled, checking API key...");
if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") {
debug!("API key found, initializing Anthropic provider");
let provider = AnthropicProvider::new(
api_key,
anthropic_config.model.clone(),
anthropic_config.temperature.unwrap_or(0.7),
anthropic_config.cost_per_1m_input_tokens.unwrap_or(3.0),
anthropic_config.cost_per_1m_output_tokens.unwrap_or(15.0),
Some(event_bus.clone()),
);
info!("Anthropic provider initialized successfully");
providers.push(Box::new(provider));
} else {
warn!("ANTHROPIC_API_KEY environment variable not set. Skipping Anthropic provider.");
}
} else {
debug!("Anthropic provider is disabled in config");
}
} else {
debug!("No Anthropic config found");
}
if let Some(ollama_config) = &config.ai_providers.ollama {
if ollama_config.enabled {
match OllamaProvider::new(
Some(ollama_config.model.clone()),
ollama_config.temperature,
ollama_config.max_tokens,
Some(event_bus.clone()),
) {
Ok(provider) => {
info!("Ollama provider initialized successfully");
providers.push(Box::new(provider));
}
Err(e) => {
warn!("Failed to initialize Ollama provider: {}. Skipping.", e);
}
}
}
}
if providers.is_empty() {
error!("No AI providers configured, using LocalProvider");
providers.push(Box::new(LocalProvider));
}
let llm_manager = Arc::new(LLMManager::new(
providers,
event_bus.clone(),
Arc::new(config.clone()),
));
context_manager.set_llm_manager(llm_manager.clone());
let context_manager = Arc::new(context_manager);
let tool_manager = Arc::new(ToolManager::from_config(config).await?);
info!("ToolManager initialized with {} tools", tool_manager.tool_names().len());
Ok((llm_manager, artifact_manager, context_manager, tool_manager))
}