pub mod cli;
pub mod commands;
pub mod config;
pub mod core;
pub mod formatters;
pub mod logging;
pub mod mcp_server;
pub mod remote;
pub mod utils;
use anyhow::Result;
use std::path::Path;
use std::sync::Arc;
use tracing::{debug, info};
pub use cli::Config;
pub use core::{cache::FileCache, context_builder::ContextOptions, walker::WalkOptions};
pub use utils::error::ContextCreatorError;
pub fn run(mut config: Config) -> Result<()> {
config.load_from_file()?;
config.validate()?;
match &config.command {
Some(cli::Commands::Search { .. }) => return commands::run_search(config),
Some(cli::Commands::Diff { .. }) => return commands::run_diff(config),
Some(cli::Commands::Telemetry { .. }) => return commands::run_telemetry(config),
Some(cli::Commands::Examples) => {
println!("{}", cli::USAGE_EXAMPLES);
return Ok(());
}
None => {} }
let _temp_dir = if let Some(repo_url) = &config.remote {
if config.verbose > 0 {
debug!(
"Starting context-creator with remote repository: {}",
repo_url
);
}
let temp_dir = crate::remote::fetch_repository(repo_url, config.verbose > 0)?;
let repo_path = crate::remote::get_repo_path(&temp_dir, repo_url)?;
config.paths = Some(vec![repo_path]);
Some(temp_dir) } else {
None
};
if config.verbose > 0 {
debug!("Starting context-creator with configuration:");
debug!(" Directories: {:?}", config.get_directories());
debug!(" Max tokens: {:?}", config.max_tokens);
debug!(" LLM tool: {}", config.llm_tool.command());
debug!(" Progress: {}", config.progress);
debug!(" Quiet: {}", config.quiet);
if let Some(output) = &config.output_file {
debug!(" Output file: {}", output.display());
}
if let Some(prompt) = config.get_prompt() {
debug!(" Prompt: {}", prompt);
}
}
if config.verbose > 0 {
debug!("Creating directory walker with options...");
}
let walk_options = WalkOptions::from_config(&config)?;
if config.verbose > 0 {
debug!("Creating context generation options...");
}
let context_options = ContextOptions::from_config(&config)?;
if config.verbose > 0 {
debug!("Creating file cache for I/O optimization...");
}
let cache = Arc::new(FileCache::new());
let mut all_outputs = Vec::new();
let directories = config.get_directories();
for (index, directory) in directories.iter().enumerate() {
if config.progress && !config.quiet && directories.len() > 1 {
info!(
"Processing directory {} of {}: {}",
index + 1,
directories.len(),
directory.display()
);
}
let output = process_directory(
directory,
walk_options.clone(),
context_options.clone(),
cache.clone(),
&config,
)?;
all_outputs.push((directory.clone(), output));
}
let output = if all_outputs.len() == 1 {
all_outputs.into_iter().next().unwrap().1
} else {
let mut combined = String::new();
combined.push_str("# Code Context - Multiple Directories\n\n");
for (path, content) in all_outputs {
combined.push_str(&format!("## Directory: {}\n\n", path.display()));
combined.push_str(&content);
combined.push_str("\n\n");
}
combined
};
let resolved_prompt = config.get_prompt();
match (
config.output_file.as_ref(),
resolved_prompt.as_ref(),
config.copy,
) {
(Some(file), None, false) => {
std::fs::write(file, output)?;
if !config.quiet {
println!(" Written to {}", file.display());
}
}
(None, Some(prompt), false) => {
if config.progress && !config.quiet {
info!("Sending context to {}...", config.llm_tool.command());
}
execute_with_llm(prompt, &output, &config)?;
}
(None, Some(prompt), true) => {
copy_to_clipboard(&output)?;
if !config.quiet {
println!("✓ Copied to clipboard");
}
if config.progress && !config.quiet {
info!("Sending context to {}...", config.llm_tool.command());
}
execute_with_llm(prompt, &output, &config)?;
}
(None, None, true) => {
copy_to_clipboard(&output)?;
if !config.quiet {
println!("✓ Copied to clipboard");
}
}
(None, None, false) => {
print!("{output}");
}
(Some(_), _, true) => {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot specify both --copy and --output".to_string(),
)
.into());
}
(Some(_), Some(_), _) => {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot specify both output file and prompt".to_string(),
)
.into());
}
}
Ok(())
}
fn process_directory(
path: &Path,
walk_options: WalkOptions,
context_options: ContextOptions,
cache: Arc<FileCache>,
config: &Config,
) -> Result<String> {
if config.progress && !config.quiet {
info!("Scanning directory: {}", path.display());
}
let mut files = core::walker::walk_directory(path, walk_options.clone())?;
if config.progress && !config.quiet {
info!("Found {} files", files.len());
}
if config.trace_imports || config.include_callers || config.include_types {
if config.progress && !config.quiet {
info!("Analyzing semantic dependencies...");
}
let project_analysis = core::project_analyzer::ProjectAnalysis::analyze_project(
path,
&walk_options,
config,
&cache,
)?;
let mut initial_files_map = std::collections::HashMap::new();
for file in files {
if let Some(analyzed_file) = project_analysis.get_file(&file.path) {
initial_files_map.insert(file.path.clone(), analyzed_file.clone());
} else {
initial_files_map.insert(file.path.clone(), file);
}
}
if config.progress && !config.quiet {
info!("Expanding file list based on semantic relationships...");
}
let files_map = core::file_expander::expand_file_list_with_context(
initial_files_map,
config,
&cache,
&walk_options,
&project_analysis.file_map,
)?;
files = files_map.into_values().collect();
let final_paths: std::collections::HashSet<_> =
files.iter().map(|f| f.path.clone()).collect();
for file in &mut files {
file.imported_by.retain(|path| final_paths.contains(path));
}
if config.progress && !config.quiet {
info!("Expanded to {} files", files.len());
}
}
if config.verbose > 0 {
debug!("File list:");
for file in &files {
debug!(
" {} ({})",
file.relative_path.display(),
file.file_type_display()
);
}
}
let prioritized_files = if context_options.max_tokens.is_some() {
if config.progress && !config.quiet {
info!("Prioritizing files for token limit...");
}
core::prioritizer::prioritize_files(files, &context_options, cache.clone())?
} else {
files
};
if config.progress && !config.quiet {
info!(
"Generating markdown from {} files...",
prioritized_files.len()
);
}
let output = if config.output_format == cli::OutputFormat::Markdown {
core::context_builder::generate_markdown(prioritized_files, context_options, cache)?
} else {
core::context_builder::generate_digest(
prioritized_files,
context_options,
cache,
config.output_format,
&path.display().to_string(),
)?
};
if config.progress && !config.quiet {
info!("Output generation complete");
}
Ok(output)
}
fn execute_with_llm(prompt: &str, context: &str, config: &Config) -> Result<()> {
use std::io::Write;
use std::process::Stdio;
let (mut command, combined_input) = config.llm_tool.prepare_command(config)?;
let stdin_data = if combined_input {
format!("{prompt}\n\n{context}") } else {
context.to_string() };
let tool_command = config.llm_tool.command();
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ContextCreatorError::LlmToolNotFound {
tool: tool_command.to_string(),
install_instructions: config.llm_tool.install_instructions().to_string(),
}
} else {
ContextCreatorError::SubprocessError(e.to_string())
}
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(stdin_data.as_bytes())?;
stdin.flush()?;
}
let status = child.wait()?;
if !status.success() {
return Err(ContextCreatorError::SubprocessError(format!(
"{tool_command} exited with status: {status}"
))
.into());
}
if !config.quiet {
info!("{} completed successfully", tool_command);
}
Ok(())
}
fn copy_to_clipboard(content: &str) -> Result<()> {
use arboard::Clipboard;
let mut clipboard = Clipboard::new().map_err(|e| {
ContextCreatorError::ClipboardError(format!("Failed to access clipboard: {e}"))
})?;
clipboard.set_text(content).map_err(|e| {
ContextCreatorError::ClipboardError(format!("Failed to copy to clipboard: {e}"))
})?;
Ok(())
}