use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use tracing::debug;
pub const USAGE_EXAMPLES: &str = "\
USAGE EXAMPLES:
Basic Usage:
# Process current directory
context-creator
# Process specific directories
context-creator src/ tests/ docs/
# Save to file
context-creator -o context.md
Pattern Matching:
# Include specific file types (quote patterns to prevent shell expansion)
context-creator --include \"**/*.py\" --include \"src/**/*.{rs,toml}\"
# Exclude patterns
context-creator --ignore \"**/*_test.py\" --ignore \"**/migrations/**\"
# Combine includes and excludes
context-creator --include \"**/*.ts\" --ignore \"node_modules/**\"
Search Command:
# Search for a term with automatic semantic analysis
context-creator search \"AuthenticationService\"
# Search without semantic analysis (faster)
context-creator search \"TODO\" --no-semantic
# Search in specific directories
context-creator search \"database\" src/ tests/
Git Diff Command:
# Compare current changes with last commit
context-creator diff HEAD~1 HEAD
# Compare two branches
context-creator diff main feature-branch
# Save diff analysis to file
context-creator --output-file changes.md diff HEAD~1 HEAD
# Apply token limits for large diffs
context-creator --max-tokens 50000 diff HEAD~5 HEAD
# Include semantic analysis of changed files
context-creator --trace-imports --include-callers diff main HEAD
Semantic Analysis:
# Trace import dependencies
context-creator --trace-imports --include \"**/auth.py\"
# Find function callers
context-creator --include-callers --include \"**/payment.ts\"
# Include type definitions
context-creator --include-types --include \"**/models/**\"
# Control traversal depth
context-creator --semantic-depth 5 --include \"src/core/**\"
LLM Integration:
# Ask questions about your codebase
context-creator --prompt \"How does authentication work?\"
# Targeted analysis
context-creator --prompt \"Review security\" --include \"src/auth/**\"
# Read prompt from stdin
echo \"Find performance issues\" | context-creator --stdin
Remote Repositories:
# Analyze GitHub repository
context-creator --repo https://github.com/owner/repo
# With specific patterns
context-creator --repo https://github.com/facebook/react --include \"**/*.js\"
Advanced Options:
# Copy to clipboard
context-creator --include \"**/*.py\" --copy
# Set token limit
context-creator --max-tokens 100000
# Verbose logging
context-creator -vv --include \"src/**\"
";
const AFTER_HELP_MSG: &str = "\
CUSTOM PRIORITY RULES:
Custom priority rules are processed in a 'first-match-wins' basis. Rules are
evaluated in the order they are defined in your .context-creator.toml configuration
file. The first rule that matches a given file will be used, and all subsequent
rules will be ignored for that file.
Example configuration:
[[priorities]]
pattern = \"src/**/*.rs\"
weight = 10.0
[[priorities]]
pattern = \"tests/*\"
weight = -2.0
For usage examples, run: context-creator examples
";
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum LlmTool {
#[value(name = "gemini")]
#[default]
Gemini,
#[value(name = "codex")]
Codex,
#[value(name = "claude")]
Claude,
#[value(name = "ollama")]
Ollama,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum LogFormat {
#[value(name = "plain")]
#[default]
Plain,
#[value(name = "json")]
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum OutputFormat {
#[value(name = "markdown")]
#[default]
Markdown,
#[value(name = "xml")]
Xml,
#[value(name = "plain")]
Plain,
#[value(name = "paths")]
Paths,
}
impl LlmTool {
pub fn command(&self) -> &'static str {
match self {
LlmTool::Gemini => "gemini",
LlmTool::Codex => "codex",
LlmTool::Claude => "claude",
LlmTool::Ollama => "ollama",
}
}
pub fn install_instructions(&self) -> &'static str {
match self {
LlmTool::Gemini => "Please install gemini with: pip install gemini",
LlmTool::Codex => {
"Please install codex CLI from: https://github.com/microsoft/codex-cli"
}
LlmTool::Claude => {
"Please install Claude Code with: npm install -g @anthropic-ai/claude-code"
}
LlmTool::Ollama => {
"Please install Ollama from: https://ollama.ai or with: brew install ollama"
}
}
}
pub fn default_max_tokens(&self) -> usize {
match self {
LlmTool::Gemini => 1_000_000,
LlmTool::Codex => 1_000_000,
LlmTool::Claude => 200_000, LlmTool::Ollama => 4_096, }
}
pub fn default_max_tokens_with_config(
&self,
config_token_limits: Option<&crate::config::TokenLimits>,
) -> usize {
if let Some(token_limits) = config_token_limits {
match self {
LlmTool::Gemini => token_limits.gemini.unwrap_or(1_000_000),
LlmTool::Codex => token_limits.codex.unwrap_or(1_000_000),
LlmTool::Claude => token_limits.claude.unwrap_or(200_000),
LlmTool::Ollama => token_limits.ollama.unwrap_or(4_096),
}
} else {
self.default_max_tokens()
}
}
pub fn prepare_command(
&self,
config: &Config,
) -> Result<(std::process::Command, bool), crate::utils::error::ContextCreatorError> {
use std::process::Command;
match self {
LlmTool::Gemini | LlmTool::Codex => {
let cmd = Command::new(self.command());
Ok((cmd, true)) }
LlmTool::Claude => {
let mut cmd = Command::new(self.command());
if let Some(prompt) = config.get_prompt() {
cmd.arg("-p").arg(prompt);
}
Ok((cmd, false)) }
LlmTool::Ollama => {
let model = config.ollama_model.as_ref().ok_or_else(|| {
crate::utils::error::ContextCreatorError::InvalidConfiguration(
"--ollama-model is required when using --tool ollama".to_string(),
)
})?;
let mut cmd = Command::new(self.command());
cmd.arg("run").arg(model);
Ok((cmd, true)) }
}
}
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
Search {
pattern: String,
#[arg(long = "no-semantic")]
no_semantic: bool,
#[arg(value_name = "PATHS")]
paths: Option<Vec<PathBuf>>,
},
Diff {
from: String,
to: String,
},
Examples,
Telemetry {
#[arg(short = 't', long = "telemetry-file", required = true)]
telemetry_file: PathBuf,
#[arg(long = "time-range")]
time_range: Option<String>,
#[arg(long = "service")]
service: Option<String>,
#[arg(value_name = "PATHS")]
paths: Option<Vec<PathBuf>>,
},
}
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None, after_help = AFTER_HELP_MSG)]
pub struct Config {
#[command(subcommand)]
pub command: Option<Commands>,
#[arg(short = 'p', long = "prompt", help = "Process a text prompt directly")]
pub prompt: Option<String>,
#[arg(value_name = "PATHS", help = "Process files and directories")]
pub paths: Option<Vec<PathBuf>>,
#[arg(
long,
help = "Include files and directories matching the given glob pattern.\nPatterns use gitignore-style syntax. To prevent shell expansion,\nquote patterns: --include \"*.py\" --include \"src/**/*.{rs,toml}\""
)]
pub include: Option<Vec<String>>,
#[arg(
long,
help = "Ignore files and directories matching the given glob pattern.\nPatterns use gitignore-style syntax. To prevent shell expansion,\nquote patterns: --ignore \"node_modules/**\" --ignore \"target/**\""
)]
pub ignore: Option<Vec<String>>,
#[arg(long, help = "Process a GitHub repository")]
pub remote: Option<String>,
#[arg(long = "stdin", help = "Read prompt from standard input")]
pub read_stdin: bool,
#[arg(short = 'o', long)]
pub output_file: Option<PathBuf>,
#[arg(long)]
pub max_tokens: Option<usize>,
#[arg(long = "tool", default_value = "gemini")]
pub llm_tool: LlmTool,
#[arg(
long = "ollama-model",
help = "Ollama model to use (e.g., llama3, codellama)"
)]
pub ollama_model: Option<String>,
#[arg(short = 'q', long)]
pub quiet: bool,
#[arg(short = 'v', long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long = "log-format", value_enum, default_value = "plain")]
pub log_format: LogFormat,
#[arg(short = 'c', long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub progress: bool,
#[arg(short = 'C', long)]
pub copy: bool,
#[arg(long = "enhanced-context")]
pub enhanced_context: bool,
#[arg(long = "git-context")]
pub git_context: bool,
#[arg(long = "git-context-depth", default_value = "3")]
pub git_context_depth: usize,
#[arg(long = "style", value_enum, default_value = "markdown")]
pub output_format: OutputFormat,
#[arg(long, help = "Include files that import the specified modules")]
pub trace_imports: bool,
#[arg(long, help = "Include files containing callers of specified functions")]
pub include_callers: bool,
#[arg(long, help = "Include type definitions and interfaces")]
pub include_types: bool,
#[arg(
long,
default_value = "5",
help = "Depth limit for dependency traversal"
)]
pub semantic_depth: usize,
#[arg(long, help = "Start MCP server mode")]
pub mcp: bool,
#[arg(
long = "mcp-port",
default_value = "9090",
help = "Port for MCP server"
)]
pub mcp_port: u16,
#[arg(long = "rmcp", help = "Use RMCP implementation for MCP server")]
pub rmcp: bool,
#[arg(
long = "rmcp-transport",
default_value = "stdio",
help = "Transport mode for RMCP server (stdio, http)"
)]
pub rmcp_transport: String,
#[clap(skip)]
pub custom_priorities: Vec<crate::config::Priority>,
#[clap(skip)]
pub config_token_limits: Option<crate::config::TokenLimits>,
#[clap(skip)]
pub config_defaults_max_tokens: Option<usize>,
}
impl Default for Config {
fn default() -> Self {
Self {
command: None,
prompt: None,
paths: None,
include: None,
ignore: None,
remote: None,
read_stdin: false,
output_file: None,
max_tokens: None,
llm_tool: LlmTool::default(),
ollama_model: None,
quiet: false,
verbose: 0,
log_format: LogFormat::default(),
config: None,
progress: false,
copy: false,
enhanced_context: false,
git_context: false,
git_context_depth: 3,
output_format: OutputFormat::default(),
trace_imports: false,
include_callers: false,
include_types: false,
semantic_depth: 5,
mcp: false,
mcp_port: 9090,
rmcp: false,
rmcp_transport: "stdio".to_string(),
custom_priorities: vec![],
config_token_limits: None,
config_defaults_max_tokens: None,
}
}
}
impl Config {
pub fn validate(&self) -> Result<(), crate::utils::error::ContextCreatorError> {
use crate::utils::error::ContextCreatorError;
if self.mcp || self.rmcp {
if self.paths.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Paths cannot be used with MCP server mode".to_string(),
));
}
if self.prompt.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Prompt cannot be used with MCP server mode".to_string(),
));
}
if self.remote.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Remote repository cannot be used with MCP server mode".to_string(),
));
}
if self.command.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Commands cannot be used with MCP server mode".to_string(),
));
}
return Ok(());
}
if self.command.is_some() {
return Ok(());
}
let has_input_source = self.get_prompt().is_some()
|| self.paths.is_some()
|| self.include.is_some()
|| self.remote.is_some()
|| self.read_stdin;
if !has_input_source {
return Err(ContextCreatorError::InvalidConfiguration(
"At least one input source must be provided: --prompt, paths, --include, --remote, or --stdin".to_string(),
));
}
if self.verbose > 0 && self.quiet {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot use both --verbose (-v) and --quiet (-q) flags together".to_string(),
));
}
if let Some(repo_url) = &self.remote {
if !repo_url.starts_with("https://github.com/")
&& !repo_url.starts_with("http://github.com/")
{
return Err(ContextCreatorError::InvalidConfiguration(
"Repository URL must be a GitHub URL (https://github.com/owner/repo)"
.to_string(),
));
}
} else {
let paths = self.get_directories();
for path in &paths {
if !path.exists() {
return Err(ContextCreatorError::InvalidPath(format!(
"Path does not exist: {}",
path.display()
)));
}
if !path.is_dir() && !path.is_file() {
return Err(ContextCreatorError::InvalidPath(format!(
"Path is neither a file nor a directory: {}",
path.display()
)));
}
}
}
if let Some(output) = &self.output_file {
if let Some(parent) = output.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
return Err(ContextCreatorError::InvalidPath(format!(
"Output directory does not exist: {}",
parent.display()
)));
}
}
}
if self.output_file.is_some() && self.get_prompt().is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot specify both --output and a prompt".to_string(),
));
}
if self.copy && self.output_file.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot specify both --copy and --output".to_string(),
));
}
if self.remote.is_some() && self.paths.is_some() {
return Err(ContextCreatorError::InvalidConfiguration(
"Cannot specify both --remote and local paths. Use --remote to analyze a remote repository, or provide local paths to analyze local directories.".to_string(),
));
}
if self.llm_tool == LlmTool::Ollama
&& self.ollama_model.is_none()
&& self.get_prompt().is_some()
{
return Err(ContextCreatorError::InvalidConfiguration(
"--ollama-model is required when using --tool ollama".to_string(),
));
}
Ok(())
}
pub fn load_from_file(&mut self) -> Result<(), crate::utils::error::ContextCreatorError> {
use crate::config::ConfigFile;
let config_file = if let Some(ref config_path) = self.config {
Some(ConfigFile::load_from_file(config_path)?)
} else {
ConfigFile::load_default()?
};
if let Some(config_file) = config_file {
self.custom_priorities = config_file.priorities.clone();
self.config_token_limits = Some(config_file.tokens.clone());
config_file.apply_to_cli_config(self);
if self.verbose > 0 {
if let Some(ref config_path) = self.config {
debug!("Loaded configuration from: {}", config_path.display());
} else {
debug!("Loaded configuration from default location");
}
}
}
Ok(())
}
pub fn get_prompt(&self) -> Option<String> {
self.prompt
.as_ref()
.filter(|s| !s.trim().is_empty())
.cloned()
}
pub fn get_directories(&self) -> Vec<PathBuf> {
if let Some(paths) = &self.paths {
paths.clone()
} else if self.include.is_some() {
vec![PathBuf::from(".")]
} else {
vec![PathBuf::from(".")]
}
}
pub fn get_include_patterns(&self) -> Vec<String> {
self.include.as_ref().cloned().unwrap_or_default()
}
pub fn get_ignore_patterns(&self) -> Vec<String> {
self.ignore.as_ref().cloned().unwrap_or_default()
}
pub fn get_effective_max_tokens(&self) -> Option<usize> {
if let Some(explicit_tokens) = self.max_tokens {
return Some(explicit_tokens);
}
if let Some(_prompt) = self.get_prompt() {
if let Some(token_limits) = &self.config_token_limits {
let config_limit = match self.llm_tool {
LlmTool::Gemini => token_limits.gemini,
LlmTool::Codex => token_limits.codex,
LlmTool::Claude => token_limits.claude,
LlmTool::Ollama => token_limits.ollama,
};
if let Some(limit) = config_limit {
return Some(limit);
}
}
if let Some(defaults_tokens) = self.config_defaults_max_tokens {
return Some(defaults_tokens);
}
return Some(self.llm_tool.default_max_tokens());
}
if let Some(defaults_tokens) = self.config_defaults_max_tokens {
return Some(defaults_tokens);
}
None
}
pub fn get_effective_context_tokens(&self) -> Option<usize> {
if let Some(max_tokens) = self.get_effective_max_tokens() {
if let Some(prompt) = self.get_prompt() {
if let Ok(counter) = crate::core::token::TokenCounter::new() {
if let Ok(prompt_tokens) = counter.count_tokens(&prompt) {
let safety_buffer = 1000; let reserved = prompt_tokens + safety_buffer;
let available = max_tokens.saturating_sub(reserved);
return Some(available);
}
}
let estimated_prompt_tokens = prompt.len().div_ceil(4); let safety_buffer = 1000;
let reserved = estimated_prompt_tokens + safety_buffer;
let available = max_tokens.saturating_sub(reserved);
Some(available)
} else {
Some(max_tokens)
}
} else {
None
}
}
pub fn should_read_stdin(&self) -> bool {
use std::io::IsTerminal;
if self.read_stdin {
return true;
}
if !std::io::stdin().is_terminal() && self.get_prompt().is_none() {
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
impl Config {
#[allow(dead_code)]
fn new_for_test(paths: Option<Vec<PathBuf>>) -> Self {
Self {
paths,
quiet: true, ..Self::default()
}
}
#[allow(dead_code)]
fn new_for_test_with_include(include: Option<Vec<String>>) -> Self {
Self {
include,
quiet: true, ..Self::default()
}
}
}
#[test]
fn test_config_validation_valid_directory() {
let temp_dir = TempDir::new().unwrap();
let config = Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validation_invalid_directory() {
let config = Config {
paths: Some(vec![PathBuf::from("/nonexistent/directory")]),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_validation_file_as_directory() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("file.txt");
fs::write(&file_path, "test").unwrap();
let config = Config {
paths: Some(vec![file_path]),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validation_invalid_output_directory() {
let temp_dir = TempDir::new().unwrap();
let config = Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
output_file: Some(PathBuf::from("/nonexistent/directory/output.md")),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_validation_mutually_exclusive_options() {
let temp_dir = TempDir::new().unwrap();
let config = Config {
prompt: Some("test prompt".to_string()),
paths: Some(vec![temp_dir.path().to_path_buf()]),
output_file: Some(temp_dir.path().join("output.md")),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_llm_tool_enum_values() {
assert_eq!(LlmTool::Gemini.command(), "gemini");
assert_eq!(LlmTool::Codex.command(), "codex");
assert!(LlmTool::Gemini
.install_instructions()
.contains("pip install"));
assert!(LlmTool::Codex.install_instructions().contains("github.com"));
assert_eq!(LlmTool::default(), LlmTool::Gemini);
}
#[test]
fn test_llm_tool_default_max_tokens() {
assert_eq!(LlmTool::Gemini.default_max_tokens(), 1_000_000);
assert_eq!(LlmTool::Codex.default_max_tokens(), 1_000_000);
}
#[test]
fn test_config_get_effective_max_tokens_with_explicit() {
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: Some(500_000),
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(500_000));
}
#[test]
fn test_config_get_effective_max_tokens_with_prompt_default() {
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
}
#[test]
fn test_config_get_effective_max_tokens_no_prompt() {
let config = Config {
prompt: None,
max_tokens: None,
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), None);
}
#[test]
fn test_config_get_effective_max_tokens_with_config_gemini() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Gemini,
config_token_limits: Some(TokenLimits {
gemini: Some(2_500_000),
codex: Some(1_800_000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(2_500_000));
}
#[test]
fn test_config_get_effective_max_tokens_with_config_codex() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Codex,
config_token_limits: Some(TokenLimits {
gemini: Some(2_500_000),
codex: Some(1_800_000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(1_800_000));
}
#[test]
fn test_config_get_effective_max_tokens_explicit_overrides_config() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: Some(500_000), llm_tool: LlmTool::Gemini,
config_token_limits: Some(TokenLimits {
gemini: Some(2_500_000),
codex: Some(1_800_000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(500_000));
}
#[test]
fn test_config_get_effective_max_tokens_config_partial_gemini() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Gemini,
config_token_limits: Some(TokenLimits {
gemini: Some(3_000_000),
codex: None, claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(3_000_000));
}
#[test]
fn test_config_get_effective_max_tokens_config_partial_codex() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Codex,
config_token_limits: Some(TokenLimits {
gemini: None, codex: Some(1_200_000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(1_200_000));
}
#[test]
fn test_config_get_effective_max_tokens_config_fallback_to_default() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("test prompt".to_string()),
max_tokens: None,
llm_tool: LlmTool::Gemini,
config_token_limits: Some(TokenLimits {
gemini: None, codex: Some(1_800_000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
}
#[test]
fn test_llm_tool_default_max_tokens_with_config() {
use crate::config::TokenLimits;
let token_limits = TokenLimits {
gemini: Some(2_500_000),
codex: Some(1_800_000),
claude: None,
ollama: None,
};
assert_eq!(
LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
2_500_000
);
assert_eq!(
LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
1_800_000
);
}
#[test]
fn test_llm_tool_default_max_tokens_with_config_partial() {
use crate::config::TokenLimits;
let token_limits = TokenLimits {
gemini: Some(3_000_000),
codex: None, claude: None,
ollama: None,
};
assert_eq!(
LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
3_000_000
);
assert_eq!(
LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
1_000_000
);
}
#[test]
fn test_llm_tool_default_max_tokens_with_no_config() {
assert_eq!(
LlmTool::Gemini.default_max_tokens_with_config(None),
1_000_000
);
assert_eq!(
LlmTool::Codex.default_max_tokens_with_config(None),
1_000_000
);
}
#[test]
fn test_get_effective_context_tokens_with_prompt() {
let config = Config {
prompt: Some("This is a test prompt".to_string()),
max_tokens: Some(10000),
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
let context_tokens = config.get_effective_context_tokens().unwrap();
assert!(context_tokens < 10000);
assert!(context_tokens > 8000); }
#[test]
fn test_get_effective_context_tokens_no_prompt() {
let config = Config {
prompt: None,
max_tokens: Some(10000),
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_context_tokens(), Some(10000));
}
#[test]
fn test_get_effective_context_tokens_no_limit() {
let config = Config {
prompt: None, max_tokens: None,
llm_tool: LlmTool::Gemini,
..Config::new_for_test(None)
};
assert_eq!(config.get_effective_context_tokens(), None);
}
#[test]
fn test_get_effective_context_tokens_with_config_limits() {
use crate::config::TokenLimits;
let config = Config {
prompt: Some("This is a longer test prompt for token counting".to_string()),
max_tokens: None, llm_tool: LlmTool::Gemini,
config_token_limits: Some(TokenLimits {
gemini: Some(50000),
codex: Some(40000),
claude: None,
ollama: None,
}),
..Config::new_for_test(None)
};
let context_tokens = config.get_effective_context_tokens().unwrap();
assert!(context_tokens < 50000);
assert!(context_tokens > 45000); }
#[test]
fn test_config_validation_output_file_in_current_dir() {
let temp_dir = TempDir::new().unwrap();
let config = Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
output_file: Some(PathBuf::from("output.md")),
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_load_from_file_no_config() {
let temp_dir = TempDir::new().unwrap();
let mut config = Config {
paths: Some(vec![temp_dir.path().to_path_buf()]),
..Default::default()
};
assert!(config.load_from_file().is_ok());
}
#[test]
fn test_parse_directories() {
use clap::Parser;
let args = vec!["context-creator", "/path/one"];
let config = Config::parse_from(args);
assert_eq!(config.paths.as_ref().unwrap().len(), 1);
assert_eq!(
config.paths.as_ref().unwrap()[0],
PathBuf::from("/path/one")
);
}
#[test]
fn test_parse_multiple_directories() {
use clap::Parser;
let args = vec!["context-creator", "/path/one", "/path/two", "/path/three"];
let config = Config::parse_from(args);
assert_eq!(config.paths.as_ref().unwrap().len(), 3);
assert_eq!(
config.paths.as_ref().unwrap()[0],
PathBuf::from("/path/one")
);
assert_eq!(
config.paths.as_ref().unwrap()[1],
PathBuf::from("/path/two")
);
assert_eq!(
config.paths.as_ref().unwrap()[2],
PathBuf::from("/path/three")
);
let args = vec!["context-creator", "--prompt", "Find duplicated patterns"];
let config = Config::parse_from(args);
assert_eq!(config.prompt, Some("Find duplicated patterns".to_string()));
}
#[test]
fn test_validate_multiple_directories() {
let temp_dir = TempDir::new().unwrap();
let dir1 = temp_dir.path().join("dir1");
let dir2 = temp_dir.path().join("dir2");
fs::create_dir(&dir1).unwrap();
fs::create_dir(&dir2).unwrap();
let config = Config {
paths: Some(vec![dir1.clone(), dir2.clone()]),
..Default::default()
};
assert!(config.validate().is_ok());
let config = Config {
paths: Some(vec![dir1, PathBuf::from("/nonexistent/dir")]),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_validate_files_as_directories() {
let temp_dir = TempDir::new().unwrap();
let dir1 = temp_dir.path().join("dir1");
let file1 = temp_dir.path().join("file.txt");
fs::create_dir(&dir1).unwrap();
fs::write(&file1, "test content").unwrap();
let config = Config {
paths: Some(vec![dir1, file1]),
..Default::default()
};
assert!(config.validate().is_ok());
}
}