1use clap::{Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5use tracing::debug;
6
7pub const USAGE_EXAMPLES: &str = "\
9USAGE EXAMPLES:
10
11Basic Usage:
12 # Process current directory
13 context-creator
14
15 # Process specific directories
16 context-creator src/ tests/ docs/
17
18 # Save to file
19 context-creator -o context.md
20
21Pattern Matching:
22 # Include specific file types (quote patterns to prevent shell expansion)
23 context-creator --include \"**/*.py\" --include \"src/**/*.{rs,toml}\"
24
25 # Exclude patterns
26 context-creator --ignore \"**/*_test.py\" --ignore \"**/migrations/**\"
27
28 # Combine includes and excludes
29 context-creator --include \"**/*.ts\" --ignore \"node_modules/**\"
30
31Search Command:
32 # Search for a term with automatic semantic analysis
33 context-creator search \"AuthenticationService\"
34
35 # Search without semantic analysis (faster)
36 context-creator search \"TODO\" --no-semantic
37
38 # Search in specific directories
39 context-creator search \"database\" src/ tests/
40
41Git Diff Command:
42 # Compare current changes with last commit
43 context-creator diff HEAD~1 HEAD
44
45 # Compare two branches
46 context-creator diff main feature-branch
47
48 # Save diff analysis to file
49 context-creator --output-file changes.md diff HEAD~1 HEAD
50
51 # Apply token limits for large diffs
52 context-creator --max-tokens 50000 diff HEAD~5 HEAD
53
54 # Include semantic analysis of changed files
55 context-creator --trace-imports --include-callers diff main HEAD
56
57Semantic Analysis:
58 # Trace import dependencies
59 context-creator --trace-imports --include \"**/auth.py\"
60
61 # Find function callers
62 context-creator --include-callers --include \"**/payment.ts\"
63
64 # Include type definitions
65 context-creator --include-types --include \"**/models/**\"
66
67 # Control traversal depth
68 context-creator --semantic-depth 5 --include \"src/core/**\"
69
70LLM Integration:
71 # Ask questions about your codebase
72 context-creator --prompt \"How does authentication work?\"
73
74 # Targeted analysis
75 context-creator --prompt \"Review security\" --include \"src/auth/**\"
76
77 # Read prompt from stdin
78 echo \"Find performance issues\" | context-creator --stdin
79
80Remote Repositories:
81 # Analyze GitHub repository
82 context-creator --repo https://github.com/owner/repo
83
84 # With specific patterns
85 context-creator --repo https://github.com/facebook/react --include \"**/*.js\"
86
87Advanced Options:
88 # Copy to clipboard
89 context-creator --include \"**/*.py\" --copy
90
91 # Set token limit
92 context-creator --max-tokens 100000
93
94 # Verbose logging
95 context-creator -vv --include \"src/**\"
96";
97
98const AFTER_HELP_MSG: &str = "\
100CUSTOM PRIORITY RULES:
101 Custom priority rules are processed in a 'first-match-wins' basis. Rules are
102 evaluated in the order they are defined in your .context-creator.toml configuration
103 file. The first rule that matches a given file will be used, and all subsequent
104 rules will be ignored for that file.
105
106 Example configuration:
107 [[priorities]]
108 pattern = \"src/**/*.rs\"
109 weight = 10.0
110
111 [[priorities]]
112 pattern = \"tests/*\"
113 weight = -2.0
114
115For usage examples, run: context-creator examples
116";
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
120pub enum LlmTool {
121 #[value(name = "gemini")]
123 #[default]
124 Gemini,
125 #[value(name = "codex")]
127 Codex,
128 #[value(name = "claude")]
130 Claude,
131 #[value(name = "ollama")]
133 Ollama,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
138pub enum LogFormat {
139 #[value(name = "plain")]
141 #[default]
142 Plain,
143 #[value(name = "json")]
145 Json,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
150pub enum OutputFormat {
151 #[value(name = "markdown")]
153 #[default]
154 Markdown,
155 #[value(name = "xml")]
157 Xml,
158 #[value(name = "plain")]
160 Plain,
161 #[value(name = "paths")]
163 Paths,
164}
165
166impl LlmTool {
167 pub fn command(&self) -> &'static str {
169 match self {
170 LlmTool::Gemini => "gemini",
171 LlmTool::Codex => "codex",
172 LlmTool::Claude => "claude",
173 LlmTool::Ollama => "ollama",
174 }
175 }
176
177 pub fn install_instructions(&self) -> &'static str {
179 match self {
180 LlmTool::Gemini => "Please install gemini with: pip install gemini",
181 LlmTool::Codex => {
182 "Please install codex CLI from: https://github.com/microsoft/codex-cli"
183 }
184 LlmTool::Claude => {
185 "Please install Claude Code with: npm install -g @anthropic-ai/claude-code"
186 }
187 LlmTool::Ollama => {
188 "Please install Ollama from: https://ollama.ai or with: brew install ollama"
189 }
190 }
191 }
192
193 pub fn default_max_tokens(&self) -> usize {
195 match self {
196 LlmTool::Gemini => 1_000_000,
197 LlmTool::Codex => 1_000_000,
198 LlmTool::Claude => 200_000, LlmTool::Ollama => 4_096, }
201 }
202
203 pub fn default_max_tokens_with_config(
205 &self,
206 config_token_limits: Option<&crate::config::TokenLimits>,
207 ) -> usize {
208 if let Some(token_limits) = config_token_limits {
209 match self {
210 LlmTool::Gemini => token_limits.gemini.unwrap_or(1_000_000),
211 LlmTool::Codex => token_limits.codex.unwrap_or(1_000_000),
212 LlmTool::Claude => token_limits.claude.unwrap_or(200_000),
213 LlmTool::Ollama => token_limits.ollama.unwrap_or(4_096),
214 }
215 } else {
216 self.default_max_tokens()
217 }
218 }
219
220 pub fn prepare_command(
223 &self,
224 config: &Config,
225 ) -> Result<(std::process::Command, bool), crate::utils::error::ContextCreatorError> {
226 use std::process::Command;
227
228 match self {
229 LlmTool::Gemini | LlmTool::Codex => {
230 let cmd = Command::new(self.command());
232 Ok((cmd, true)) }
234 LlmTool::Claude => {
235 let mut cmd = Command::new(self.command());
237 if let Some(prompt) = config.get_prompt() {
238 cmd.arg("-p").arg(prompt);
239 }
240 Ok((cmd, false)) }
242 LlmTool::Ollama => {
243 let model = config.ollama_model.as_ref().ok_or_else(|| {
245 crate::utils::error::ContextCreatorError::InvalidConfiguration(
246 "--ollama-model is required when using --tool ollama".to_string(),
247 )
248 })?;
249 let mut cmd = Command::new(self.command());
250 cmd.arg("run").arg(model);
251 Ok((cmd, true)) }
253 }
254 }
255}
256
257#[derive(Subcommand, Debug, Clone)]
259pub enum Commands {
260 Search {
262 pattern: String,
264
265 #[arg(long = "no-semantic")]
267 no_semantic: bool,
268
269 #[arg(value_name = "PATHS")]
271 paths: Option<Vec<PathBuf>>,
272 },
273
274 Diff {
276 from: String,
278
279 to: String,
281 },
282
283 Examples,
285
286 Telemetry {
288 #[arg(short = 't', long = "telemetry-file", required = true)]
290 telemetry_file: PathBuf,
291
292 #[arg(long = "time-range")]
294 time_range: Option<String>,
295
296 #[arg(long = "service")]
298 service: Option<String>,
299
300 #[arg(value_name = "PATHS")]
302 paths: Option<Vec<PathBuf>>,
303 },
304}
305
306#[derive(Parser, Debug, Clone)]
308#[command(author, version, about, long_about = None, after_help = AFTER_HELP_MSG)]
309pub struct Config {
310 #[command(subcommand)]
312 pub command: Option<Commands>,
313 #[arg(short = 'p', long = "prompt", help = "Process a text prompt directly")]
315 pub prompt: Option<String>,
316
317 #[arg(value_name = "PATHS", help = "Process files and directories")]
320 pub paths: Option<Vec<PathBuf>>,
321
322 #[arg(
325 long,
326 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}\""
327 )]
328 pub include: Option<Vec<String>>,
329
330 #[arg(
332 long,
333 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/**\""
334 )]
335 pub ignore: Option<Vec<String>>,
336
337 #[arg(long, help = "Process a GitHub repository")]
339 pub remote: Option<String>,
340
341 #[arg(long = "stdin", help = "Read prompt from standard input")]
343 pub read_stdin: bool,
344
345 #[arg(short = 'o', long)]
347 pub output_file: Option<PathBuf>,
348
349 #[arg(long)]
351 pub max_tokens: Option<usize>,
352
353 #[arg(long = "tool", default_value = "gemini")]
355 pub llm_tool: LlmTool,
356
357 #[arg(
359 long = "ollama-model",
360 help = "Ollama model to use (e.g., llama3, codellama)"
361 )]
362 pub ollama_model: Option<String>,
363
364 #[arg(short = 'q', long)]
366 pub quiet: bool,
367
368 #[arg(short = 'v', long, action = clap::ArgAction::Count)]
370 pub verbose: u8,
371
372 #[arg(long = "log-format", value_enum, default_value = "plain")]
374 pub log_format: LogFormat,
375
376 #[arg(short = 'c', long)]
378 pub config: Option<PathBuf>,
379
380 #[arg(long)]
382 pub progress: bool,
383
384 #[arg(short = 'C', long)]
386 pub copy: bool,
387
388 #[arg(long = "enhanced-context")]
390 pub enhanced_context: bool,
391
392 #[arg(long = "git-context")]
394 pub git_context: bool,
395
396 #[arg(long = "git-context-depth", default_value = "3")]
398 pub git_context_depth: usize,
399
400 #[arg(long = "style", value_enum, default_value = "markdown")]
402 pub output_format: OutputFormat,
403
404 #[arg(long, help = "Include files that import the specified modules")]
406 pub trace_imports: bool,
407
408 #[arg(long, help = "Include files containing callers of specified functions")]
410 pub include_callers: bool,
411
412 #[arg(long, help = "Include type definitions and interfaces")]
414 pub include_types: bool,
415
416 #[arg(
418 long,
419 default_value = "5",
420 help = "Depth limit for dependency traversal"
421 )]
422 pub semantic_depth: usize,
423
424 #[arg(long, help = "Start MCP server mode")]
426 pub mcp: bool,
427
428 #[arg(
430 long = "mcp-port",
431 default_value = "9090",
432 help = "Port for MCP server"
433 )]
434 pub mcp_port: u16,
435
436 #[arg(long = "rmcp", help = "Use RMCP implementation for MCP server")]
438 pub rmcp: bool,
439
440 #[arg(
442 long = "rmcp-transport",
443 default_value = "stdio",
444 help = "Transport mode for RMCP server (stdio, http)"
445 )]
446 pub rmcp_transport: String,
447
448 #[clap(skip)]
450 pub custom_priorities: Vec<crate::config::Priority>,
451
452 #[clap(skip)]
454 pub config_token_limits: Option<crate::config::TokenLimits>,
455
456 #[clap(skip)]
458 pub config_defaults_max_tokens: Option<usize>,
459}
460
461impl Default for Config {
462 fn default() -> Self {
463 Self {
464 command: None,
465 prompt: None,
466 paths: None,
467 include: None,
468 ignore: None,
469 remote: None,
470 read_stdin: false,
471 output_file: None,
472 max_tokens: None,
473 llm_tool: LlmTool::default(),
474 ollama_model: None,
475 quiet: false,
476 verbose: 0,
477 log_format: LogFormat::default(),
478 config: None,
479 progress: false,
480 copy: false,
481 enhanced_context: false,
482 git_context: false,
483 git_context_depth: 3,
484 output_format: OutputFormat::default(),
485 trace_imports: false,
486 include_callers: false,
487 include_types: false,
488 semantic_depth: 5,
489 mcp: false,
490 mcp_port: 9090,
491 rmcp: false,
492 rmcp_transport: "stdio".to_string(),
493 custom_priorities: vec![],
494 config_token_limits: None,
495 config_defaults_max_tokens: None,
496 }
497 }
498}
499
500impl Config {
501 pub fn validate(&self) -> Result<(), crate::utils::error::ContextCreatorError> {
503 use crate::utils::error::ContextCreatorError;
504
505 if self.mcp || self.rmcp {
507 if self.paths.is_some() {
508 return Err(ContextCreatorError::InvalidConfiguration(
509 "Paths cannot be used with MCP server mode".to_string(),
510 ));
511 }
512 if self.prompt.is_some() {
513 return Err(ContextCreatorError::InvalidConfiguration(
514 "Prompt cannot be used with MCP server mode".to_string(),
515 ));
516 }
517 if self.remote.is_some() {
518 return Err(ContextCreatorError::InvalidConfiguration(
519 "Remote repository cannot be used with MCP server mode".to_string(),
520 ));
521 }
522 if self.command.is_some() {
523 return Err(ContextCreatorError::InvalidConfiguration(
524 "Commands cannot be used with MCP server mode".to_string(),
525 ));
526 }
527 return Ok(());
529 }
530
531 if self.command.is_some() {
533 return Ok(());
534 }
535
536 let has_input_source = self.get_prompt().is_some()
538 || self.paths.is_some()
539 || self.include.is_some()
540 || self.remote.is_some()
541 || self.read_stdin;
542
543 if !has_input_source {
544 return Err(ContextCreatorError::InvalidConfiguration(
545 "At least one input source must be provided: --prompt, paths, --include, --remote, or --stdin".to_string(),
546 ));
547 }
548
549 if self.verbose > 0 && self.quiet {
551 return Err(ContextCreatorError::InvalidConfiguration(
552 "Cannot use both --verbose (-v) and --quiet (-q) flags together".to_string(),
553 ));
554 }
555
556 if let Some(repo_url) = &self.remote {
570 if !repo_url.starts_with("https://github.com/")
571 && !repo_url.starts_with("http://github.com/")
572 {
573 return Err(ContextCreatorError::InvalidConfiguration(
574 "Repository URL must be a GitHub URL (https://github.com/owner/repo)"
575 .to_string(),
576 ));
577 }
578 } else {
579 let paths = self.get_directories();
581 for path in &paths {
582 if !path.exists() {
583 return Err(ContextCreatorError::InvalidPath(format!(
584 "Path does not exist: {}",
585 path.display()
586 )));
587 }
588
589 if !path.is_dir() && !path.is_file() {
591 return Err(ContextCreatorError::InvalidPath(format!(
592 "Path is neither a file nor a directory: {}",
593 path.display()
594 )));
595 }
596 }
597 }
598
599 if let Some(output) = &self.output_file {
604 if let Some(parent) = output.parent() {
605 if !parent.as_os_str().is_empty() && !parent.exists() {
607 return Err(ContextCreatorError::InvalidPath(format!(
608 "Output directory does not exist: {}",
609 parent.display()
610 )));
611 }
612 }
613 }
614
615 if self.output_file.is_some() && self.get_prompt().is_some() {
617 return Err(ContextCreatorError::InvalidConfiguration(
618 "Cannot specify both --output and a prompt".to_string(),
619 ));
620 }
621
622 if self.copy && self.output_file.is_some() {
624 return Err(ContextCreatorError::InvalidConfiguration(
625 "Cannot specify both --copy and --output".to_string(),
626 ));
627 }
628
629 if self.remote.is_some() && self.paths.is_some() {
633 return Err(ContextCreatorError::InvalidConfiguration(
634 "Cannot specify both --remote and local paths. Use --remote to analyze a remote repository, or provide local paths to analyze local directories.".to_string(),
635 ));
636 }
637
638 if self.llm_tool == LlmTool::Ollama
640 && self.ollama_model.is_none()
641 && self.get_prompt().is_some()
642 {
643 return Err(ContextCreatorError::InvalidConfiguration(
644 "--ollama-model is required when using --tool ollama".to_string(),
645 ));
646 }
647
648 Ok(())
649 }
650
651 pub fn load_from_file(&mut self) -> Result<(), crate::utils::error::ContextCreatorError> {
653 use crate::config::ConfigFile;
654
655 let config_file = if let Some(ref config_path) = self.config {
656 Some(ConfigFile::load_from_file(config_path)?)
658 } else {
659 ConfigFile::load_default()?
661 };
662
663 if let Some(config_file) = config_file {
664 self.custom_priorities = config_file.priorities.clone();
666
667 self.config_token_limits = Some(config_file.tokens.clone());
669
670 config_file.apply_to_cli_config(self);
671
672 if self.verbose > 0 {
673 if let Some(ref config_path) = self.config {
674 debug!("Loaded configuration from: {}", config_path.display());
675 } else {
676 debug!("Loaded configuration from default location");
677 }
678 }
679 }
680
681 Ok(())
682 }
683
684 pub fn get_prompt(&self) -> Option<String> {
686 self.prompt
687 .as_ref()
688 .filter(|s| !s.trim().is_empty())
689 .cloned()
690 }
691
692 pub fn get_directories(&self) -> Vec<PathBuf> {
696 if let Some(paths) = &self.paths {
698 paths.clone()
699 } else if self.include.is_some() {
700 vec![PathBuf::from(".")]
702 } else {
703 vec![PathBuf::from(".")]
705 }
706 }
707
708 pub fn get_include_patterns(&self) -> Vec<String> {
710 self.include.as_ref().cloned().unwrap_or_default()
711 }
712
713 pub fn get_ignore_patterns(&self) -> Vec<String> {
715 self.ignore.as_ref().cloned().unwrap_or_default()
716 }
717
718 pub fn get_effective_max_tokens(&self) -> Option<usize> {
720 if let Some(explicit_tokens) = self.max_tokens {
722 return Some(explicit_tokens);
723 }
724
725 if let Some(_prompt) = self.get_prompt() {
727 if let Some(token_limits) = &self.config_token_limits {
729 let config_limit = match self.llm_tool {
730 LlmTool::Gemini => token_limits.gemini,
731 LlmTool::Codex => token_limits.codex,
732 LlmTool::Claude => token_limits.claude,
733 LlmTool::Ollama => token_limits.ollama,
734 };
735
736 if let Some(limit) = config_limit {
737 return Some(limit);
738 }
739 }
740
741 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
743 return Some(defaults_tokens);
744 }
745
746 return Some(self.llm_tool.default_max_tokens());
748 }
749
750 if let Some(defaults_tokens) = self.config_defaults_max_tokens {
752 return Some(defaults_tokens);
753 }
754
755 None
757 }
758
759 pub fn get_effective_context_tokens(&self) -> Option<usize> {
762 if let Some(max_tokens) = self.get_effective_max_tokens() {
763 if let Some(prompt) = self.get_prompt() {
764 if let Ok(counter) = crate::core::token::TokenCounter::new() {
766 if let Ok(prompt_tokens) = counter.count_tokens(&prompt) {
767 let safety_buffer = 1000; let reserved = prompt_tokens + safety_buffer;
770 let available = max_tokens.saturating_sub(reserved);
771 return Some(available);
772 }
773 }
774 let estimated_prompt_tokens = prompt.len().div_ceil(4); let safety_buffer = 1000;
777 let reserved = estimated_prompt_tokens + safety_buffer;
778 let available = max_tokens.saturating_sub(reserved);
779 Some(available)
780 } else {
781 Some(max_tokens)
783 }
784 } else {
785 None
786 }
787 }
788
789 pub fn should_read_stdin(&self) -> bool {
791 use std::io::IsTerminal;
792
793 if self.read_stdin {
795 return true;
796 }
797
798 if !std::io::stdin().is_terminal() && self.get_prompt().is_none() {
800 return true;
801 }
802
803 false
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810 use std::fs;
811 use tempfile::TempDir;
812
813 impl Config {
814 #[allow(dead_code)]
816 fn new_for_test(paths: Option<Vec<PathBuf>>) -> Self {
817 Self {
818 paths,
819 quiet: true, ..Self::default()
821 }
822 }
823
824 #[allow(dead_code)]
826 fn new_for_test_with_include(include: Option<Vec<String>>) -> Self {
827 Self {
828 include,
829 quiet: true, ..Self::default()
831 }
832 }
833 }
834
835 #[test]
836 fn test_config_validation_valid_directory() {
837 let temp_dir = TempDir::new().unwrap();
838 let config = Config {
839 paths: Some(vec![temp_dir.path().to_path_buf()]),
840 ..Default::default()
841 };
842
843 assert!(config.validate().is_ok());
844 }
845
846 #[test]
847 fn test_config_validation_invalid_directory() {
848 let config = Config {
849 paths: Some(vec![PathBuf::from("/nonexistent/directory")]),
850 ..Default::default()
851 };
852
853 assert!(config.validate().is_err());
854 }
855
856 #[test]
857 fn test_config_validation_file_as_directory() {
858 let temp_dir = TempDir::new().unwrap();
859 let file_path = temp_dir.path().join("file.txt");
860 fs::write(&file_path, "test").unwrap();
861
862 let config = Config {
863 paths: Some(vec![file_path]),
864 ..Default::default()
865 };
866
867 assert!(config.validate().is_ok());
869 }
870
871 #[test]
872 fn test_config_validation_invalid_output_directory() {
873 let temp_dir = TempDir::new().unwrap();
874 let config = Config {
875 paths: Some(vec![temp_dir.path().to_path_buf()]),
876 output_file: Some(PathBuf::from("/nonexistent/directory/output.md")),
877 ..Default::default()
878 };
879
880 assert!(config.validate().is_err());
881 }
882
883 #[test]
884 fn test_config_validation_mutually_exclusive_options() {
885 let temp_dir = TempDir::new().unwrap();
886 let config = Config {
887 prompt: Some("test prompt".to_string()),
888 paths: Some(vec![temp_dir.path().to_path_buf()]),
889 output_file: Some(temp_dir.path().join("output.md")),
890 ..Default::default()
891 };
892
893 assert!(config.validate().is_err());
894 }
895
896 #[test]
897 fn test_llm_tool_enum_values() {
898 assert_eq!(LlmTool::Gemini.command(), "gemini");
899 assert_eq!(LlmTool::Codex.command(), "codex");
900
901 assert!(LlmTool::Gemini
902 .install_instructions()
903 .contains("pip install"));
904 assert!(LlmTool::Codex.install_instructions().contains("github.com"));
905
906 assert_eq!(LlmTool::default(), LlmTool::Gemini);
907 }
908
909 #[test]
910 fn test_llm_tool_default_max_tokens() {
911 assert_eq!(LlmTool::Gemini.default_max_tokens(), 1_000_000);
912 assert_eq!(LlmTool::Codex.default_max_tokens(), 1_000_000);
913 }
914
915 #[test]
916 fn test_config_get_effective_max_tokens_with_explicit() {
917 let config = Config {
918 prompt: Some("test prompt".to_string()),
919 max_tokens: Some(500_000),
920 llm_tool: LlmTool::Gemini,
921 ..Config::new_for_test(None)
922 };
923 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
924 }
925
926 #[test]
927 fn test_config_get_effective_max_tokens_with_prompt_default() {
928 let config = Config {
929 prompt: Some("test prompt".to_string()),
930 max_tokens: None,
931 llm_tool: LlmTool::Gemini,
932 ..Config::new_for_test(None)
933 };
934 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
935 }
936
937 #[test]
938 fn test_config_get_effective_max_tokens_no_prompt() {
939 let config = Config {
940 prompt: None,
941 max_tokens: None,
942 llm_tool: LlmTool::Gemini,
943 ..Config::new_for_test(None)
944 };
945 assert_eq!(config.get_effective_max_tokens(), None);
946 }
947
948 #[test]
949 fn test_config_get_effective_max_tokens_with_config_gemini() {
950 use crate::config::TokenLimits;
951
952 let config = Config {
953 prompt: Some("test prompt".to_string()),
954 max_tokens: None,
955 llm_tool: LlmTool::Gemini,
956 config_token_limits: Some(TokenLimits {
957 gemini: Some(2_500_000),
958 codex: Some(1_800_000),
959 claude: None,
960 ollama: None,
961 }),
962 ..Config::new_for_test(None)
963 };
964 assert_eq!(config.get_effective_max_tokens(), Some(2_500_000));
965 }
966
967 #[test]
968 fn test_config_get_effective_max_tokens_with_config_codex() {
969 use crate::config::TokenLimits;
970
971 let config = Config {
972 prompt: Some("test prompt".to_string()),
973 max_tokens: None,
974 llm_tool: LlmTool::Codex,
975 config_token_limits: Some(TokenLimits {
976 gemini: Some(2_500_000),
977 codex: Some(1_800_000),
978 claude: None,
979 ollama: None,
980 }),
981 ..Config::new_for_test(None)
982 };
983 assert_eq!(config.get_effective_max_tokens(), Some(1_800_000));
984 }
985
986 #[test]
987 fn test_config_get_effective_max_tokens_explicit_overrides_config() {
988 use crate::config::TokenLimits;
989
990 let config = Config {
991 prompt: Some("test prompt".to_string()),
992 max_tokens: Some(500_000), llm_tool: LlmTool::Gemini,
994 config_token_limits: Some(TokenLimits {
995 gemini: Some(2_500_000),
996 codex: Some(1_800_000),
997 claude: None,
998 ollama: None,
999 }),
1000 ..Config::new_for_test(None)
1001 };
1002 assert_eq!(config.get_effective_max_tokens(), Some(500_000));
1003 }
1004
1005 #[test]
1006 fn test_config_get_effective_max_tokens_config_partial_gemini() {
1007 use crate::config::TokenLimits;
1008
1009 let config = Config {
1010 prompt: Some("test prompt".to_string()),
1011 max_tokens: None,
1012 llm_tool: LlmTool::Gemini,
1013 config_token_limits: Some(TokenLimits {
1014 gemini: Some(3_000_000),
1015 codex: None, claude: None,
1017 ollama: None,
1018 }),
1019 ..Config::new_for_test(None)
1020 };
1021 assert_eq!(config.get_effective_max_tokens(), Some(3_000_000));
1022 }
1023
1024 #[test]
1025 fn test_config_get_effective_max_tokens_config_partial_codex() {
1026 use crate::config::TokenLimits;
1027
1028 let config = Config {
1029 prompt: Some("test prompt".to_string()),
1030 max_tokens: None,
1031 llm_tool: LlmTool::Codex,
1032 config_token_limits: Some(TokenLimits {
1033 gemini: None, codex: Some(1_200_000),
1035 claude: None,
1036 ollama: None,
1037 }),
1038 ..Config::new_for_test(None)
1039 };
1040 assert_eq!(config.get_effective_max_tokens(), Some(1_200_000));
1041 }
1042
1043 #[test]
1044 fn test_config_get_effective_max_tokens_config_fallback_to_default() {
1045 use crate::config::TokenLimits;
1046
1047 let config = Config {
1048 prompt: Some("test prompt".to_string()),
1049 max_tokens: None,
1050 llm_tool: LlmTool::Gemini,
1051 config_token_limits: Some(TokenLimits {
1052 gemini: None, codex: Some(1_800_000),
1054 claude: None,
1055 ollama: None,
1056 }),
1057 ..Config::new_for_test(None)
1058 };
1059 assert_eq!(config.get_effective_max_tokens(), Some(1_000_000));
1061 }
1062
1063 #[test]
1064 fn test_llm_tool_default_max_tokens_with_config() {
1065 use crate::config::TokenLimits;
1066
1067 let token_limits = TokenLimits {
1068 gemini: Some(2_500_000),
1069 codex: Some(1_800_000),
1070 claude: None,
1071 ollama: None,
1072 };
1073
1074 assert_eq!(
1075 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
1076 2_500_000
1077 );
1078 assert_eq!(
1079 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
1080 1_800_000
1081 );
1082 }
1083
1084 #[test]
1085 fn test_llm_tool_default_max_tokens_with_config_partial() {
1086 use crate::config::TokenLimits;
1087
1088 let token_limits = TokenLimits {
1089 gemini: Some(3_000_000),
1090 codex: None, claude: None,
1092 ollama: None,
1093 };
1094
1095 assert_eq!(
1096 LlmTool::Gemini.default_max_tokens_with_config(Some(&token_limits)),
1097 3_000_000
1098 );
1099 assert_eq!(
1101 LlmTool::Codex.default_max_tokens_with_config(Some(&token_limits)),
1102 1_000_000
1103 );
1104 }
1105
1106 #[test]
1107 fn test_llm_tool_default_max_tokens_with_no_config() {
1108 assert_eq!(
1109 LlmTool::Gemini.default_max_tokens_with_config(None),
1110 1_000_000
1111 );
1112 assert_eq!(
1113 LlmTool::Codex.default_max_tokens_with_config(None),
1114 1_000_000
1115 );
1116 }
1117
1118 #[test]
1119 fn test_get_effective_context_tokens_with_prompt() {
1120 let config = Config {
1121 prompt: Some("This is a test prompt".to_string()),
1122 max_tokens: Some(10000),
1123 llm_tool: LlmTool::Gemini,
1124 ..Config::new_for_test(None)
1125 };
1126
1127 let context_tokens = config.get_effective_context_tokens().unwrap();
1128 assert!(context_tokens < 10000);
1130 assert!(context_tokens > 8000); }
1133
1134 #[test]
1135 fn test_get_effective_context_tokens_no_prompt() {
1136 let config = Config {
1137 prompt: None,
1138 max_tokens: Some(10000),
1139 llm_tool: LlmTool::Gemini,
1140 ..Config::new_for_test(None)
1141 };
1142
1143 assert_eq!(config.get_effective_context_tokens(), Some(10000));
1145 }
1146
1147 #[test]
1148 fn test_get_effective_context_tokens_no_limit() {
1149 let config = Config {
1150 prompt: None, max_tokens: None,
1152 llm_tool: LlmTool::Gemini,
1153 ..Config::new_for_test(None)
1154 };
1155
1156 assert_eq!(config.get_effective_context_tokens(), None);
1158 }
1159
1160 #[test]
1161 fn test_get_effective_context_tokens_with_config_limits() {
1162 use crate::config::TokenLimits;
1163
1164 let config = Config {
1165 prompt: Some("This is a longer test prompt for token counting".to_string()),
1166 max_tokens: None, llm_tool: LlmTool::Gemini,
1168 config_token_limits: Some(TokenLimits {
1169 gemini: Some(50000),
1170 codex: Some(40000),
1171 claude: None,
1172 ollama: None,
1173 }),
1174 ..Config::new_for_test(None)
1175 };
1176
1177 let context_tokens = config.get_effective_context_tokens().unwrap();
1178 assert!(context_tokens < 50000);
1180 assert!(context_tokens > 45000); }
1182
1183 #[test]
1184 fn test_config_validation_output_file_in_current_dir() {
1185 let temp_dir = TempDir::new().unwrap();
1186 let config = Config {
1187 paths: Some(vec![temp_dir.path().to_path_buf()]),
1188 output_file: Some(PathBuf::from("output.md")),
1189 ..Default::default()
1190 };
1191
1192 assert!(config.validate().is_ok());
1194 }
1195
1196 #[test]
1197 fn test_config_load_from_file_no_config() {
1198 let temp_dir = TempDir::new().unwrap();
1199 let mut config = Config {
1200 paths: Some(vec![temp_dir.path().to_path_buf()]),
1201 ..Default::default()
1202 };
1203
1204 assert!(config.load_from_file().is_ok());
1206 }
1207
1208 #[test]
1209 fn test_parse_directories() {
1210 use clap::Parser;
1211
1212 let args = vec!["context-creator", "/path/one"];
1214 let config = Config::parse_from(args);
1215 assert_eq!(config.paths.as_ref().unwrap().len(), 1);
1216 assert_eq!(
1217 config.paths.as_ref().unwrap()[0],
1218 PathBuf::from("/path/one")
1219 );
1220 }
1221
1222 #[test]
1223 fn test_parse_multiple_directories() {
1224 use clap::Parser;
1225
1226 let args = vec!["context-creator", "/path/one", "/path/two", "/path/three"];
1228 let config = Config::parse_from(args);
1229 assert_eq!(config.paths.as_ref().unwrap().len(), 3);
1230 assert_eq!(
1231 config.paths.as_ref().unwrap()[0],
1232 PathBuf::from("/path/one")
1233 );
1234 assert_eq!(
1235 config.paths.as_ref().unwrap()[1],
1236 PathBuf::from("/path/two")
1237 );
1238 assert_eq!(
1239 config.paths.as_ref().unwrap()[2],
1240 PathBuf::from("/path/three")
1241 );
1242
1243 let args = vec!["context-creator", "--prompt", "Find duplicated patterns"];
1245 let config = Config::parse_from(args);
1246 assert_eq!(config.prompt, Some("Find duplicated patterns".to_string()));
1247 }
1248
1249 #[test]
1250 fn test_validate_multiple_directories() {
1251 let temp_dir = TempDir::new().unwrap();
1252 let dir1 = temp_dir.path().join("dir1");
1253 let dir2 = temp_dir.path().join("dir2");
1254 fs::create_dir(&dir1).unwrap();
1255 fs::create_dir(&dir2).unwrap();
1256
1257 let config = Config {
1259 paths: Some(vec![dir1.clone(), dir2.clone()]),
1260 ..Default::default()
1261 };
1262 assert!(config.validate().is_ok());
1263
1264 let config = Config {
1266 paths: Some(vec![dir1, PathBuf::from("/nonexistent/dir")]),
1267 ..Default::default()
1268 };
1269 assert!(config.validate().is_err());
1270 }
1271
1272 #[test]
1273 fn test_validate_files_as_directories() {
1274 let temp_dir = TempDir::new().unwrap();
1275 let dir1 = temp_dir.path().join("dir1");
1276 let file1 = temp_dir.path().join("file.txt");
1277 fs::create_dir(&dir1).unwrap();
1278 fs::write(&file1, "test content").unwrap();
1279
1280 let config = Config {
1282 paths: Some(vec![dir1, file1]),
1283 ..Default::default()
1284 };
1285 assert!(config.validate().is_ok());
1286 }
1287}