#![warn(missing_docs, unused_crate_dependencies)]
mod config;
use cai_core::{Entry, Metadata, Source};
use cai_ingest::{IngestConfig, Ingestor};
use cai_output::{Formatter, StatsFormatter};
use cai_storage::Storage;
use chrono::{Duration, Utc};
use clap::{Parser, Subcommand};
use colored::Colorize;
use config::load_config;
use std::path::PathBuf;
use std::sync::Arc;
async fn create_storage_with_mock_data() -> cai_storage::MemoryStorage {
let storage = cai_storage::MemoryStorage::new();
let mock_entries = vec![
Entry {
id: "1".to_string(),
source: Source::Claude,
timestamp: Utc::now() - Duration::hours(2),
prompt: "Help me refactor this Rust function to be more idiomatic".to_string(),
response: "Here's a more idiomatic version using iterators and pattern matching..."
.to_string(),
metadata: Metadata {
file_path: Some("src/main.rs".to_string()),
language: Some("Rust".to_string()),
..Default::default()
},
},
Entry {
id: "2".to_string(),
source: Source::Claude,
timestamp: Utc::now() - Duration::hours(4),
prompt: "Write a unit test for this module".to_string(),
response: "Here are comprehensive unit tests using rstest...".to_string(),
metadata: Metadata {
file_path: Some("src/storage.rs".to_string()),
language: Some("Rust".to_string()),
..Default::default()
},
},
Entry {
id: "3".to_string(),
source: Source::Git,
timestamp: Utc::now() - Duration::days(1),
prompt: "feat: add user authentication".to_string(),
response: "Implemented OAuth2 flow with session management".to_string(),
metadata: Metadata {
commit_hash: Some("abc123def456".to_string()),
..Default::default()
},
},
Entry {
id: "4".to_string(),
source: Source::Codex,
timestamp: Utc::now() - Duration::days(2),
prompt: "Generate a function to parse JSON".to_string(),
response: "Here's a JSON parsing function using serde_json...".to_string(),
metadata: Metadata {
file_path: Some("src/parser.rs".to_string()),
language: Some("Rust".to_string()),
..Default::default()
},
},
];
for entry in mock_entries {
if let Err(e) = storage.store(&entry).await {
tracing::warn!("Failed to store mock entry {}: {}", entry.id, e);
}
}
storage
}
fn format_with_formatter<F: Formatter>(
results: &[Entry],
formatter: F,
format_name: &str,
) -> cai_core::Result<String> {
let mut buffer = Vec::new();
formatter.format(results, &mut buffer)?;
String::from_utf8(buffer).map_err(|e| {
cai_core::Error::Message(format!("Invalid UTF-8 in {} output: {}", format_name, e))
})
}
#[derive(Parser, Clone)]
#[command(name = "cai")]
#[command(about = "Superior AI coding history analyzer", long_about = None)]
#[command(version = "0.1.0")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Clone)]
enum Commands {
Query {
query: String,
#[arg(short, long, default_value = "table")]
output: String,
},
Ingest {
#[arg(short, long)]
source: String,
#[arg(short, long)]
path: Option<String>,
},
Stats,
Schema {
#[arg(short, long)]
table: Option<String>,
},
Tui,
Web {
#[arg(short, long, default_value = "3000")]
port: u16,
},
}
async fn execute_ingest(source: &str, path: Option<&str>) -> cai_core::Result<()> {
println!("{} {}", "Ingesting from:".green(), source);
let config = match source.to_lowercase().as_str() {
"claude" => IngestConfig {
parse_claude: true,
parse_codex: false,
scan_git: false,
claude_dir: path.map(PathBuf::from),
..Default::default()
},
"codex" => IngestConfig {
parse_claude: false,
parse_codex: true,
scan_git: false,
codex_file: path.map(PathBuf::from),
..Default::default()
},
"all" => IngestConfig {
parse_claude: true,
parse_codex: true,
scan_git: false,
claude_dir: path.map(PathBuf::from),
codex_file: path.map(PathBuf::from),
..Default::default()
},
_ => {
return Err(cai_core::Error::Message(format!(
"Unknown source: '{}'. Valid options: claude, codex, all",
source
)));
}
};
let ingestor = Ingestor::new(config);
let storage = cai_storage::MemoryStorage::new();
let count = match ingestor.ingest_all(&storage).await {
Ok(count) => count,
Err(e) => {
eprintln!("{} {}", "Error:".red(), e);
std::process::exit(1);
}
};
println!("\n{} {} entries", "Successfully ingested:".green(), count);
Ok(())
}
async fn execute_stats() -> cai_core::Result<()> {
let storage = cai_storage::MemoryStorage::new();
let entries = match storage.query(None as Option<&cai_storage::Filter>).await {
Ok(entries) => entries,
Err(e) => {
eprintln!("{} {}", "Error:".red(), e);
std::process::exit(1);
}
};
println!("\n{} {} entries", "Found:".cyan(), entries.len());
if entries.is_empty() {
println!("\n{}", "No entries found.".dimmed());
return Ok(());
}
let formatter = StatsFormatter::default();
let mut buffer = Vec::new();
formatter.format(&entries, &mut buffer)?;
let output = String::from_utf8(buffer)
.map_err(|e| cai_core::Error::Message(format!("Invalid UTF-8 in stats output: {}", e)))?;
println!("\n{}", output);
Ok(())
}
async fn execute_schema(table: Option<&str>) -> cai_core::Result<()> {
println!("\n{}", "Database Schema".bold().cyan());
println!("{}", "=================".cyan());
println!("\n{}", "Available Tables:".bold().green());
println!(" - {}", "entries".bold());
if let Some(table_name) = table {
if table_name.to_lowercase() == "entries" {
println!("\n{}", format!("Table: {}", table_name).bold().green());
println!("────────────────────────────────────────────────────────────────────");
println!("{:<20} {:<20} {:<40}", "Column", "Type", "Description");
println!("────────────────────────────────────────────────────────────────────");
println!("{:<20} {:<20} {:<40}", "id", "TEXT", "Unique identifier");
println!(
"{:<20} {:<20} {:<40}",
"source", "TEXT", "Source system (Claude, Codex, Git, Other)"
);
println!(
"{:<20} {:<20} {:<40}",
"timestamp", "TIMESTAMP", "Interaction timestamp (UTC)"
);
println!(
"{:<20} {:<20} {:<40}",
"prompt", "TEXT", "User prompt/input"
);
println!(
"{:<20} {:<20} {:<40}",
"response", "TEXT", "AI response/output"
);
println!(
"{:<20} {:<20} {:<40}",
"metadata", "JSON", "Additional metadata (file_path, language, etc.)"
);
println!("────────────────────────────────────────────────────────────────────");
} else {
return Err(cai_core::Error::Message(format!(
"Unknown table: '{}'. Available tables: entries",
table_name
)));
}
} else {
println!("\n{}", "Columns in 'entries' table:".bold());
println!(" id - Unique identifier (TEXT)");
println!(" source - Source system (TEXT)");
println!(" timestamp - Interaction timestamp (TIMESTAMP)");
println!(" prompt - User prompt/input (TEXT)");
println!(" response - AI response/output (TEXT)");
println!(" metadata - Additional metadata (JSON)");
}
println!("\n{}", "Query Examples:".bold().green());
println!(" SHOW TABLES");
println!(" DESCRIBE entries");
println!(" SELECT * FROM entries LIMIT 10");
println!(" SELECT * FROM entries WHERE source = 'Claude'");
Ok(())
}
async fn execute_query(query: &str, output_format: &str) -> cai_core::Result<()> {
println!("{} {}", "Executing query:".green(), query.dimmed());
let storage = create_storage_with_mock_data().await;
let query_engine = cai_query::QueryEngine::new(storage);
let results = query_engine
.execute(query)
.await
.map_err(|e| cai_core::Error::Message(format!("Query execution failed: {}", e)))?;
println!("\n{} {} results", "Found:".cyan(), results.len());
if results.is_empty() {
println!("\n{}", "No results found.".dimmed());
return Ok(());
}
let output = match output_format.to_lowercase().as_str() {
"json" => format_with_formatter(&results, cai_output::JsonFormatter::default(), "json")?,
"jsonl" => format_with_formatter(&results, cai_output::JsonlFormatter::default(), "jsonl")?,
"csv" => format_with_formatter(&results, cai_output::CsvFormatter::default(), "csv")?,
"table" => format_with_formatter(&results, cai_output::TableFormatter::default(), "table")?,
"ai" => format_with_formatter(&results, cai_output::AiFormatter::default(), "ai")?,
"stats" => format_with_formatter(&results, cai_output::StatsFormatter::default(), "stats")?,
_ => {
return Err(cai_core::Error::Message(format!(
"Unknown output format: '{}'. Valid options: json, jsonl, csv, table, ai, stats",
output_format
)));
}
};
println!("\n{}", output);
Ok(())
}
#[tokio::main]
async fn main() -> cai_core::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into()),
)
.init();
let app_config = load_config();
tracing::debug!(
"Loaded config: storage type = {}",
app_config.storage.r#type
);
let cli = Cli::parse();
match cli.command {
Commands::Query { query, output } => execute_query(&query, &output).await,
Commands::Ingest { source, path } => execute_ingest(&source, path.as_deref()).await,
Commands::Stats => execute_stats().await,
Commands::Schema { table } => execute_schema(table.as_deref()).await,
Commands::Tui => {
let storage = Arc::new(create_storage_with_mock_data().await);
cai_tui::run(storage).await
}
#[cfg(feature = "web")]
Commands::Web { port } => {
let web_config = cai_web::Config {
port,
host: "127.0.0.1".to_string(),
};
println!("{} {}", "Starting web server on port:".green(), port);
let storage = std::sync::Arc::new(cai_storage::MemoryStorage::new());
cai_web::run(storage, web_config).await
}
#[cfg(not(feature = "web"))]
Commands::Web { .. } => {
eprintln!(
"{}",
"Web feature not enabled. Build with --features web.".red()
);
Err(cai_core::Error::Message(
"Web feature not enabled".to_string(),
))
}
}
}