use std::path::PathBuf;
use crate::executors::{CliExecutor, CodexExecutor, GeminiExecutor, QwenExecutor};
use crate::types::config::Config;
use crate::TetradResult;
pub async fn init(path: Option<PathBuf>) -> TetradResult<()> {
let target_dir = path.unwrap_or_else(|| PathBuf::from("."));
if !target_dir.exists() {
std::fs::create_dir_all(&target_dir)?;
tracing::info!("Directory created: {}", target_dir.display());
}
let config_path = target_dir.join("tetrad.toml");
if config_path.exists() {
println!("Configuration already exists at: {}", config_path.display());
println!("Use 'tetrad config' to modify.");
return Ok(());
}
let tetrad_dir = target_dir.join(".tetrad");
if !tetrad_dir.exists() {
std::fs::create_dir_all(&tetrad_dir)?;
tracing::info!(".tetrad/ directory created");
}
update_gitignore(&target_dir)?;
let config = Config::default_config();
config.save(&config_path)?;
println!("Tetrad initialized successfully!");
println!("Configuration created at: {}", config_path.display());
println!("Data directory: .tetrad/");
println!();
println!("Next steps:");
println!(" 1. Check if CLIs are installed: tetrad status");
println!(" 2. Configure options: tetrad config");
println!(" 3. Add to Claude Code: claude mcp add tetrad -- tetrad serve");
Ok(())
}
fn update_gitignore(target_dir: &PathBuf) -> TetradResult<()> {
let gitignore_path = target_dir.join(".gitignore");
let tetrad_entry = ".tetrad/";
let tetrad_comment = "# Tetrad - local database and cache";
if gitignore_path.exists() {
let content = std::fs::read_to_string(&gitignore_path)?;
if content.lines().any(|line| line.trim() == tetrad_entry || line.trim() == ".tetrad") {
tracing::debug!(".gitignore already contains .tetrad/");
return Ok(());
}
let mut new_content = content.trim_end().to_string();
if !new_content.is_empty() {
new_content.push_str("\n\n");
}
new_content.push_str(tetrad_comment);
new_content.push('\n');
new_content.push_str(tetrad_entry);
new_content.push('\n');
std::fs::write(&gitignore_path, new_content)?;
println!(".gitignore updated with .tetrad/");
} else {
let content = format!("{}\n{}\n", tetrad_comment, tetrad_entry);
std::fs::write(&gitignore_path, content)?;
println!(".gitignore created with .tetrad/");
}
Ok(())
}
pub async fn serve(port: Option<u16>, config: &Config) -> TetradResult<()> {
use crate::mcp::McpServer;
tracing::debug!(
"Configuração carregada: timeout={}s, consenso={:?}",
config.general.timeout_secs,
config.consensus.default_rule
);
if let Some(p) = port {
tracing::warn!("HTTP transport na porta {} ainda não implementado", p);
eprintln!("Aviso: HTTP transport ainda não suportado. Use stdio (sem --port).");
return Ok(());
}
tracing::info!("Iniciando servidor MCP Tetrad via stdio...");
let mut server = McpServer::new(config.clone())?;
server.run().await
}
pub async fn status(config: &Config) -> TetradResult<()> {
println!("Verificando status dos executores...\n");
let executors: Vec<(Box<dyn CliExecutor>, bool)> = vec![
(
Box::new(CodexExecutor::from_config(&config.executors.codex)),
config.executors.codex.enabled,
),
(
Box::new(GeminiExecutor::from_config(&config.executors.gemini)),
config.executors.gemini.enabled,
),
(
Box::new(QwenExecutor::from_config(&config.executors.qwen)),
config.executors.qwen.enabled,
),
];
for (executor, enabled) in executors {
let name = executor.name();
if !enabled {
println!(" ○ {} - desabilitado", name);
continue;
}
let available = executor.is_available().await;
let status_icon = if available { "✓" } else { "✗" };
let status_text = if available {
"disponível"
} else {
"não encontrado"
};
println!(" {} {} - {}", status_icon, name, status_text);
if available {
if let Ok(version) = executor.version().await {
println!(" versão: {}", version);
}
}
}
println!();
println!("Dica: Instale as CLIs faltantes para habilitar o consenso completo.");
Ok(())
}
pub async fn config_cmd(config_path: &PathBuf) -> TetradResult<()> {
use super::interactive::{run_interactive_config, show_config_summary};
if config_path.exists() {
let config = Config::load(config_path)?;
show_config_summary(&config);
}
run_interactive_config(config_path)
}
pub async fn doctor(config: &Config) -> TetradResult<()> {
println!("Diagnosticando configuração do Tetrad...\n");
let mut issues: Vec<String> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
println!("✓ Configuração carregada");
let executors: Vec<(Box<dyn CliExecutor>, bool, &str)> = vec![
(
Box::new(CodexExecutor::from_config(&config.executors.codex)),
config.executors.codex.enabled,
"Codex",
),
(
Box::new(GeminiExecutor::from_config(&config.executors.gemini)),
config.executors.gemini.enabled,
"Gemini",
),
(
Box::new(QwenExecutor::from_config(&config.executors.qwen)),
config.executors.qwen.enabled,
"Qwen",
),
];
let mut available_count = 0;
let mut enabled_count = 0;
for (executor, enabled, name) in executors {
if !enabled {
println!("○ {} está desabilitado no config", name);
continue;
}
enabled_count += 1;
if executor.is_available().await {
available_count += 1;
println!(
"✓ {} está disponível (comando: {})",
name,
executor.command()
);
} else {
warnings.push(format!(
"{} não está instalado (comando esperado: {})",
name,
executor.command()
));
}
}
if enabled_count == 0 {
issues.push("Nenhum executor habilitado no config - consenso não é possível".to_string());
} else if available_count == 0 {
issues.push("Nenhum executor disponível - consenso não é possível".to_string());
} else if available_count < enabled_count {
warnings.push(format!(
"Apenas {}/{} executores habilitados estão disponíveis",
available_count, enabled_count
));
}
println!();
if issues.is_empty() && warnings.is_empty() {
println!("✓ Tudo OK! Tetrad está pronto para uso.");
} else {
if !warnings.is_empty() {
println!("Avisos:");
for warning in warnings {
println!(" ⚠ {}", warning);
}
}
if !issues.is_empty() {
println!("Problemas:");
for issue in issues {
println!(" ✗ {}", issue);
}
}
}
Ok(())
}
pub fn version() {
println!("tetrad {}", env!("CARGO_PKG_VERSION"));
println!();
println!("MCP de Consenso Quádruplo para Claude Code");
println!("https://github.com/SamoraDC/tetrad");
}
pub async fn evaluate(code: &str, language: &str, config: &Config) -> TetradResult<()> {
use crate::consensus::ConsensusEngine;
use crate::reasoning::{PatternMatcher, ReasoningBank};
use crate::types::requests::{EvaluationRequest, EvaluationType};
use crate::types::responses::ModelVote;
use std::collections::HashMap;
println!("Avaliando código...\n");
let (code_content, file_path_opt) = if let Some(file_path) = code.strip_prefix('@') {
(
std::fs::read_to_string(file_path)?,
Some(file_path.to_string()),
)
} else {
(code.to_string(), None)
};
let detected_language = if language == "auto" {
PatternMatcher::detect_language(&code_content)
} else {
language.to_string()
};
println!("Linguagem: {}", detected_language);
let db_path = &config.reasoning.db_path;
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut bank = if config.reasoning.enabled {
ReasoningBank::new_with_config(db_path, &config.reasoning).ok()
} else {
None
};
if let Some(ref b) = bank {
let matches = b.retrieve(&code_content, &detected_language);
if !matches.is_empty() {
println!("\nPatterns encontrados no ReasoningBank:");
for m in &matches {
let icon = match m.pattern.pattern_type {
crate::reasoning::PatternType::AntiPattern => "⚠",
crate::reasoning::PatternType::GoodPattern => "✓",
crate::reasoning::PatternType::Ambiguous => "?",
};
println!(
" {} {} - {} (confiança: {:.0}%)",
icon,
m.pattern.issue_category,
m.pattern.description,
m.pattern.confidence * 100.0
);
}
}
}
let executors: Vec<Box<dyn CliExecutor>> = vec![
Box::new(CodexExecutor::from_config(&config.executors.codex)),
Box::new(GeminiExecutor::from_config(&config.executors.gemini)),
Box::new(QwenExecutor::from_config(&config.executors.qwen)),
];
let mut votes: HashMap<String, ModelVote> = HashMap::new();
let request_id = format!("eval-{}", chrono::Utc::now().timestamp());
let request = EvaluationRequest {
request_id: request_id.clone(),
code: code_content.clone(),
language: detected_language.clone(),
evaluation_type: EvaluationType::Code,
context: None,
file_path: file_path_opt,
};
println!("\nExecutando avaliadores...");
for executor in executors {
let name = executor.name();
if !executor.is_available().await {
println!(" {} - não disponível, pulando", name);
continue;
}
print!(" {} - avaliando... ", name);
match executor.evaluate(&request).await {
Ok(vote) => {
println!("{:?} (score: {})", vote.vote, vote.score);
votes.insert(name.to_string(), vote);
}
Err(e) => {
println!("erro: {}", e);
}
}
}
if votes.is_empty() {
println!("\nNenhum avaliador disponível. Instale pelo menos uma CLI.");
return Ok(());
}
let engine = ConsensusEngine::new(config.consensus.clone());
let result = engine.evaluate(votes, &request_id);
if let Some(ref mut b) = bank {
let loops_to_consensus = 1; match b.judge(
&request_id,
&code_content,
&detected_language,
&result,
loops_to_consensus,
config.consensus.max_loops,
) {
Ok(judgment) => {
if judgment.new_patterns_created > 0 || judgment.patterns_updated > 0 {
println!(
"\nReasoningBank: {} patterns novos, {} atualizados",
judgment.new_patterns_created, judgment.patterns_updated
);
}
}
Err(e) => {
tracing::warn!("Erro ao registrar no ReasoningBank: {}", e);
}
}
if let Ok(eval_count) = b.count_trajectories() {
if eval_count > 0 && eval_count % config.reasoning.consolidation_interval == 0 {
if let Ok(consolidation) = b.consolidate() {
if consolidation.patterns_merged > 0 || consolidation.patterns_pruned > 0 {
println!(
"ReasoningBank consolidado: {} merged, {} pruned",
consolidation.patterns_merged, consolidation.patterns_pruned
);
}
}
}
}
}
println!("\n{}", "=".repeat(50));
println!("{}", result.feedback);
println!("Score final: {}", result.score);
println!(
"Consenso: {}",
if result.consensus_achieved {
"SIM"
} else {
"NÃO"
}
);
Ok(())
}
pub async fn history(limit: usize, config: &Config) -> TetradResult<()> {
use crate::reasoning::ReasoningBank;
if !config.reasoning.enabled {
println!("ReasoningBank está desabilitado na configuração.");
return Ok(());
}
let db_path = &config.reasoning.db_path;
if !db_path.exists() {
println!("ReasoningBank ainda não foi criado.");
println!("Execute 'tetrad evaluate' para começar a coletar dados.");
return Ok(());
}
let bank = ReasoningBank::new_with_config(db_path, &config.reasoning)?;
let knowledge = bank.distill();
println!("ReasoningBank - Conhecimento Destilado\n");
println!("Total de patterns: {}", knowledge.total_patterns);
println!("Total de trajetórias: {}", knowledge.total_trajectories);
println!(
"Média de loops para consenso: {:.2}",
knowledge.avg_loops_to_consensus
);
if !knowledge.top_antipatterns.is_empty() {
println!("\nTop Anti-patterns:");
for (i, pattern) in knowledge.top_antipatterns.iter().take(limit).enumerate() {
println!(
" {}. {} ({}) - {} falhas, {:.0}% confiança",
i + 1,
pattern.issue_category,
pattern.language,
pattern.failure_count,
pattern.confidence * 100.0
);
}
}
if !knowledge.top_good_patterns.is_empty() {
println!("\nTop Good Patterns:");
for (i, pattern) in knowledge.top_good_patterns.iter().take(limit).enumerate() {
println!(
" {}. {} ({}) - {} sucessos, {:.0}% confiança",
i + 1,
pattern.issue_category,
pattern.language,
pattern.success_count,
pattern.confidence * 100.0
);
}
}
if !knowledge.language_stats.is_empty() {
println!("\nEstatísticas por linguagem:");
for (lang, stats) in &knowledge.language_stats {
println!(
" {}: {} avaliações, {:.0}% sucesso, score médio {:.1}",
lang,
stats.total_evaluations,
stats.success_rate * 100.0,
stats.avg_score
);
}
}
Ok(())
}
pub async fn export_patterns(output: &std::path::Path, config: &Config) -> TetradResult<()> {
use crate::reasoning::ReasoningBank;
if !config.reasoning.enabled {
println!("ReasoningBank está desabilitado na configuração.");
return Ok(());
}
let db_path = &config.reasoning.db_path;
if !db_path.exists() {
println!("ReasoningBank ainda não foi criado.");
println!("Nenhum pattern para exportar.");
return Ok(());
}
let bank = ReasoningBank::new_with_config(db_path, &config.reasoning)?;
bank.export(output)?;
println!("Patterns exportados para: {}", output.display());
Ok(())
}
pub async fn import_patterns(input: &std::path::Path, config: &Config) -> TetradResult<()> {
use crate::reasoning::ReasoningBank;
if !config.reasoning.enabled {
println!("ReasoningBank está desabilitado na configuração.");
return Ok(());
}
if !input.exists() {
println!("Arquivo não encontrado: {}", input.display());
return Ok(());
}
let db_path = &config.reasoning.db_path;
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut bank = ReasoningBank::new_with_config(db_path, &config.reasoning)?;
let result = bank.import(input)?;
println!("Importação concluída:");
println!(" Patterns importados: {}", result.imported);
println!(" Patterns ignorados (já existentes): {}", result.skipped);
println!(" Patterns mesclados: {}", result.merged);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_version() {
version();
}
#[tokio::test]
async fn test_status() {
let config = Config::default_config();
let result = status(&config).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_doctor() {
let config = Config::default_config();
let result = doctor(&config).await;
assert!(result.is_ok());
}
}