diff --git a/.gitignore b/.gitignore index b239c5b..33c597a 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ Thumbs.db *.tmp *.log *.bak +# BMAD (local only) +.bmad-core/ +.bmad-*/ diff --git a/codewiki/cli/adapters/doc_generator.py b/codewiki/cli/adapters/doc_generator.py index 826b60c..0d0d624 100644 --- a/codewiki/cli/adapters/doc_generator.py +++ b/codewiki/cli/adapters/doc_generator.py @@ -26,34 +26,37 @@ class CLIDocumentationGenerator: """ CLI adapter for documentation generation with progress reporting. - + This class wraps the backend documentation generator and adds CLI-specific features like progress tracking and error handling. """ - + def __init__( self, repo_path: Path, output_dir: Path, config: Dict[str, Any], verbose: bool = False, - generate_html: bool = False + generate_html: bool = False, + target_file: str = None ): """ Initialize the CLI documentation generator. - + Args: repo_path: Repository path output_dir: Output directory config: LLM configuration verbose: Enable verbose output generate_html: Whether to generate HTML viewer + target_file: Optional path to a single file for focused documentation """ self.repo_path = repo_path self.output_dir = output_dir self.config = config self.verbose = verbose self.generate_html = generate_html + self.target_file = target_file self.progress_tracker = ProgressTracker(total_stages=5, verbose=verbose) self.job = DocumentationJob() @@ -141,7 +144,10 @@ def generate(self) -> DocumentationJob: max_token_per_module=self.config.get('max_token_per_module', 36369), max_token_per_leaf_module=self.config.get('max_token_per_leaf_module', 16000), max_depth=self.config.get('max_depth', 2), - agent_instructions=self.config.get('agent_instructions') + agent_instructions=self.config.get('agent_instructions'), + target_file=self.target_file, + use_claude_code=self.config.get('use_claude_code', False), + use_gemini_code=self.config.get('use_gemini_code', False), ) # Run backend documentation generation @@ -196,29 +202,50 @@ async def _run_backend_generation(self, backend_config: BackendConfig): # Stage 2: Module Clustering self.progress_tracker.start_stage(2, "Module Clustering") - if self.verbose: - self.progress_tracker.update_stage(0.5, "Clustering modules with LLM...") - - # Import clustering function + + # Determine clustering method based on config + use_claude_code = backend_config.use_claude_code + use_gemini_code = backend_config.use_gemini_code + if use_claude_code: + if self.verbose: + self.progress_tracker.update_stage(0.5, "Clustering modules with Claude Code CLI...") + elif use_gemini_code: + if self.verbose: + self.progress_tracker.update_stage(0.5, "Clustering modules with Gemini CLI...") + else: + if self.verbose: + self.progress_tracker.update_stage(0.5, "Clustering modules with LLM...") + + # Import clustering functions from codewiki.src.be.cluster_modules import cluster_modules from codewiki.src.utils import file_manager from codewiki.src.config import FIRST_MODULE_TREE_FILENAME, MODULE_TREE_FILENAME - + working_dir = str(self.output_dir.absolute()) file_manager.ensure_directory(working_dir) first_module_tree_path = os.path.join(working_dir, FIRST_MODULE_TREE_FILENAME) module_tree_path = os.path.join(working_dir, MODULE_TREE_FILENAME) - + try: if os.path.exists(first_module_tree_path): module_tree = file_manager.load_json(first_module_tree_path) else: - module_tree = cluster_modules(leaf_nodes, components, backend_config) + if use_claude_code: + # Use Claude Code CLI for clustering + from codewiki.src.be.claude_code_adapter import claude_code_cluster + module_tree = claude_code_cluster(leaf_nodes, components, backend_config) + elif use_gemini_code: + # Use Gemini CLI for clustering (larger context window) + from codewiki.src.be.gemini_code_adapter import gemini_code_cluster + module_tree = gemini_code_cluster(leaf_nodes, components, backend_config) + else: + # Use standard LLM clustering + module_tree = cluster_modules(leaf_nodes, components, backend_config) file_manager.save_json(module_tree, first_module_tree_path) - + file_manager.save_json(module_tree, module_tree_path) self.job.module_count = len(module_tree) - + if self.verbose: self.progress_tracker.update_stage(1.0, f"Created {len(module_tree)} modules") except Exception as e: diff --git a/codewiki/cli/commands/generate.py b/codewiki/cli/commands/generate.py index 8512f73..18467b5 100644 --- a/codewiki/cli/commands/generate.py +++ b/codewiki/cli/commands/generate.py @@ -47,6 +47,13 @@ def parse_patterns(patterns_str: str) -> List[str]: default="docs", help="Output directory for generated documentation (default: ./docs)", ) +@click.option( + "--file", + "-f", + type=click.Path(exists=True), + default=None, + help="Generate documentation for a single file instead of the entire repository", +) @click.option( "--create-branch", is_flag=True, @@ -78,7 +85,6 @@ def parse_patterns(patterns_str: str) -> List[str]: ) @click.option( "--focus", - "-f", type=str, default=None, help="Comma-separated modules/paths to focus on (e.g., 'src/core,src/api')", @@ -126,10 +132,21 @@ def parse_patterns(patterns_str: str) -> List[str]: default=None, help="Maximum depth for hierarchical decomposition (overrides config)", ) +@click.option( + "--use-claude-code", + is_flag=True, + help="Use Claude Code CLI as the LLM backend instead of direct API calls", +) +@click.option( + "--use-gemini-code", + is_flag=True, + help="Use Gemini CLI as the LLM backend instead of direct API calls (supports larger context)", +) @click.pass_context def generate_command( ctx, output: str, + file: Optional[str], create_branch: bool, github_pages: bool, no_cache: bool, @@ -142,24 +159,30 @@ def generate_command( max_tokens: Optional[int], max_token_per_module: Optional[int], max_token_per_leaf_module: Optional[int], - max_depth: Optional[int] + max_depth: Optional[int], + use_claude_code: bool, + use_gemini_code: bool, ): """ Generate comprehensive documentation for a code repository. - + Analyzes the current repository and generates documentation using LLM-powered analysis. Documentation is output to ./docs/ by default. - + Examples: - + \b # Basic generation $ codewiki generate - + + \b + # Generate documentation for a single file + $ codewiki generate --file src/main.py + \b # With git branch creation and GitHub Pages $ codewiki generate --create-branch --github-pages - + \b # Force full regeneration $ codewiki generate --no-cache @@ -187,6 +210,14 @@ def generate_command( \b # Override max depth for hierarchical decomposition $ codewiki generate --max-depth 3 + + \b + # Use Claude Code CLI as the LLM backend + $ codewiki generate --use-claude-code + + \b + # Use Gemini CLI as the LLM backend (larger context window) + $ codewiki generate --use-gemini-code """ logger = create_logger(verbose=verbose) start_time = time.time() @@ -216,9 +247,46 @@ def generate_command( config = config_manager.get_config() api_key = config_manager.get_api_key() - + logger.success("Configuration valid") - + + # Validate that only one CLI backend is selected + if use_claude_code and use_gemini_code: + raise ConfigurationError( + "Cannot use both --use-claude-code and --use-gemini-code.\n\n" + "Please select only one CLI backend." + ) + + # Validate Claude Code CLI if flag is set + if use_claude_code: + import shutil + claude_path = shutil.which("claude") + if not claude_path: + raise ConfigurationError( + "Claude Code CLI not found.\n\n" + "The --use-claude-code flag requires Claude Code CLI to be installed.\n\n" + "To install Claude Code CLI, see: https://docs.anthropic.com/en/docs/claude-code\n" + "Make sure 'claude' is available in your PATH." + ) + if verbose: + logger.debug(f"Claude Code CLI found: {claude_path}") + logger.success("Claude Code CLI available") + + # Validate Gemini CLI if flag is set + if use_gemini_code: + import shutil + gemini_path = shutil.which("gemini") + if not gemini_path: + raise ConfigurationError( + "Gemini CLI not found.\n\n" + "The --use-gemini-code flag requires Gemini CLI to be installed.\n\n" + "To install Gemini CLI: npm install -g @anthropic-ai/gemini-cli\n" + "Make sure 'gemini' is available in your PATH." + ) + if verbose: + logger.debug(f"Gemini CLI found: {gemini_path}") + logger.success("Gemini CLI available") + # Validate repository logger.step("Validating repository...", 2, 4) @@ -342,6 +410,14 @@ def generate_command( elif config.agent_instructions and not config.agent_instructions.is_empty(): agent_instructions_dict = config.agent_instructions.to_dict() + # Log Claude Code mode if enabled + if use_claude_code and verbose: + logger.debug("Claude Code CLI mode enabled") + + # Log Gemini Code mode if enabled + if use_gemini_code and verbose: + logger.debug("Gemini CLI mode enabled (large context window)") + # Create generator generator = CLIDocumentationGenerator( repo_path=repo_path, @@ -359,9 +435,13 @@ def generate_command( 'max_token_per_leaf_module': max_token_per_leaf_module if max_token_per_leaf_module is not None else config.max_token_per_leaf_module, # Max depth setting (runtime override takes precedence) 'max_depth': max_depth if max_depth is not None else config.max_depth, + # CLI integrations + 'use_claude_code': use_claude_code, + 'use_gemini_code': use_gemini_code, }, verbose=verbose, - generate_html=github_pages + generate_html=github_pages, + target_file=str(file) if file else None ) # Run generation diff --git a/codewiki/src/be/agent_tools/generate_sub_module_documentations.py b/codewiki/src/be/agent_tools/generate_sub_module_documentations.py index a40b3f4..2335b11 100644 --- a/codewiki/src/be/agent_tools/generate_sub_module_documentations.py +++ b/codewiki/src/be/agent_tools/generate_sub_module_documentations.py @@ -1,4 +1,5 @@ from pydantic_ai import RunContext, Tool, Agent +from typing import Union, Any from codewiki.src.be.agent_tools.deps import CodeWikiDeps from codewiki.src.be.agent_tools.read_code_components import read_code_components_tool @@ -12,16 +13,54 @@ logger = logging.getLogger(__name__) +def normalize_sub_module_specs(specs: Union[dict[str, list[str]], list[dict]]) -> dict[str, list[str]]: + """Normalize sub_module_specs to dict format. + + Handles both formats: + - Dict format (Claude): {"module_name": ["comp1", "comp2"], ...} + - List format (GPT/Azure OpenAI): [{"name": "module_name", "components": ["comp1", "comp2"]}, ...] + + Also handles variations in key names that GPT models might use. + """ + if isinstance(specs, dict): + return specs + + if isinstance(specs, list): + result = {} + for item in specs: + if isinstance(item, dict): + # Try different key names that GPT models might use + name = item.get('name') or item.get('module_name') or item.get('sub_module_name') or item.get('submodule_name') + components = item.get('components') or item.get('core_components') or item.get('core_component_ids') or item.get('files') or [] + + if name: + result[name] = components if isinstance(components, list) else [components] + return result + + # Fallback: return empty dict + logger.warning(f"Unexpected sub_module_specs format: {type(specs)}") + return {} + + async def generate_sub_module_documentation( ctx: RunContext[CodeWikiDeps], - sub_module_specs: dict[str, list[str]] + sub_module_specs: dict[str, list[str]] | list[dict[str, Any]] ) -> str: """Generate detailed description of a given sub-module specs to the sub-agents Args: - sub_module_specs: The specs of the sub-modules to generate documentation for. E.g. {"sub_module_1": ["core_component_1.1", "core_component_1.2"], "sub_module_2": ["core_component_2.1", "core_component_2.2"], ...} + sub_module_specs: The specs of the sub-modules to generate documentation for. + Accepts two formats: + - Dict format: {"sub_module_1": ["core_component_1.1", "core_component_1.2"], "sub_module_2": ["core_component_2.1", "core_component_2.2"], ...} + - List format: [{"name": "sub_module_1", "components": ["core_component_1.1", "core_component_1.2"]}, ...] """ + # Normalize the input to dict format (handles both Claude and GPT model outputs) + sub_module_specs = normalize_sub_module_specs(sub_module_specs) + + if not sub_module_specs: + logger.warning("No valid sub-module specs provided after normalization") + return "No valid sub-module specs provided." deps = ctx.deps previous_module_name = deps.current_module_name diff --git a/codewiki/src/be/claude_code_adapter.py b/codewiki/src/be/claude_code_adapter.py new file mode 100644 index 0000000..ce929a9 --- /dev/null +++ b/codewiki/src/be/claude_code_adapter.py @@ -0,0 +1,386 @@ +""" +Claude Code CLI adapter for CodeWiki. + +This module provides functions to invoke Claude Code CLI as an alternative LLM backend +for module clustering and documentation generation. + +## Usage + +The adapter invokes Claude Code CLI in non-interactive mode: + claude --print --dangerously-skip-permissions -p - + +Prompts are passed via stdin (not command line args) to support large prompts. + +## Prompt Size Limits + +Claude Code CLI has a prompt size limit of approximately: +- **~790,000 characters** +- **~198,000 tokens** (estimated at ~4 chars/token) + +When exceeded, CLI returns exit code 1 with message: "Prompt is too long" + +The adapter validates prompt size before sending and raises `ClaudeCodeError` +if the prompt exceeds the configurable `max_prompt_tokens` limit (default: 180K tokens). + +## Error Handling + +- `ClaudeCodeError`: Raised for all CLI failures (not found, timeout, exit code != 0, prompt too large) +- Timeout: Configurable via `claude_code_timeout` in config (default: 300s) +""" + +import json +import logging +import shutil +import subprocess +from typing import Any, Dict, List, Optional + +from codewiki.src.be.dependency_analyzer.models.core import Node +from codewiki.src.be.prompt_template import ( + CLUSTER_REPO_PROMPT, + CLUSTER_MODULE_PROMPT, + format_user_prompt, + format_system_prompt, + format_leaf_system_prompt, +) +from codewiki.src.be.cluster_modules import format_potential_core_components +from codewiki.src.be.utils import is_complex_module + +logger = logging.getLogger(__name__) + +# Default timeout for Claude Code CLI (seconds) +# Increased from 300s to 900s (15 min) for larger modules +DEFAULT_CLAUDE_CODE_TIMEOUT = 900 + +# Default max prompt size (in estimated tokens) +# Claude Code CLI limit is ~790K chars (~198K tokens) +# Setting to 180K to leave room for response and system prompt +DEFAULT_MAX_PROMPT_TOKENS = 180_000 + + +class ClaudeCodeError(Exception): + """Exception raised when Claude Code CLI invocation fails.""" + + def __init__(self, message: str, returncode: Optional[int] = None, stderr: Optional[str] = None): + super().__init__(message) + self.returncode = returncode + self.stderr = stderr + + +def _find_claude_code_cli(config_path: Optional[str] = None) -> str: + """ + Find the Claude Code CLI executable. + + Args: + config_path: Optional configured path to claude CLI + + Returns: + Path to claude CLI executable + + Raises: + ClaudeCodeError: If CLI cannot be found + """ + if config_path: + if shutil.which(config_path): + return config_path + raise ClaudeCodeError(f"Claude Code CLI not found at configured path: {config_path}") + + # Try default 'claude' in PATH + claude_path = shutil.which("claude") + if claude_path: + return claude_path + + raise ClaudeCodeError( + "Claude Code CLI not found in PATH. " + "Please install Claude Code CLI or configure the path with 'codewiki config set --claude-code-path '" + ) + + +def _invoke_claude_code( + prompt: str, + timeout: int = DEFAULT_CLAUDE_CODE_TIMEOUT, + claude_code_path: Optional[str] = None, + working_dir: Optional[str] = None, + max_prompt_tokens: int = DEFAULT_MAX_PROMPT_TOKENS, +) -> str: + """ + Invoke Claude Code CLI with a prompt and return the output. + + Args: + prompt: The prompt to send to Claude Code + timeout: Timeout in seconds (default: 300) + claude_code_path: Optional path to claude CLI executable + working_dir: Optional working directory for the subprocess + max_prompt_tokens: Maximum allowed prompt size in estimated tokens (default: 150K) + + Returns: + The stdout output from Claude Code CLI + + Raises: + ClaudeCodeError: If CLI invocation fails or prompt exceeds size limit + """ + # Calculate prompt size metrics first + prompt_chars = len(prompt) + prompt_tokens_estimate = prompt_chars // 4 # Rough estimate: ~4 chars per token + + logger.info(f"Prompt size: {prompt_chars:,} chars (~{prompt_tokens_estimate:,} tokens estimated)") + + # Check prompt size limit before invoking CLI + if prompt_tokens_estimate > max_prompt_tokens: + raise ClaudeCodeError( + f"Prompt too large: ~{prompt_tokens_estimate:,} tokens estimated, " + f"max allowed: {max_prompt_tokens:,} tokens. " + f"Consider reducing the scope or splitting the request." + ) + + # Warn if prompt is approaching the limit (over 66% of max) + if prompt_tokens_estimate > max_prompt_tokens * 0.66: + logger.warning( + f"Large prompt: ~{prompt_tokens_estimate:,} tokens " + f"({prompt_tokens_estimate * 100 // max_prompt_tokens}% of {max_prompt_tokens:,} limit)" + ) + + cli_path = _find_claude_code_cli(claude_code_path) + + # Build command - use --print for non-interactive mode + # --dangerously-skip-permissions allows automated execution without interactive prompts + # Prompt is passed via stdin to handle large prompts (CLI args have size limits) + cmd = [cli_path, "--print", "--dangerously-skip-permissions", "-p", "-"] + + logger.info(f"Invoking Claude Code CLI: {cli_path}") + + try: + # Inherit environment and add any Claude-specific env vars + import os + env = os.environ.copy() + + result = subprocess.run( + cmd, + input=prompt, # Pass prompt via stdin + capture_output=True, + text=True, + timeout=timeout, + cwd=working_dir, + env=env, # Pass environment variables including CLAUDE_CODE_OAUTH_TOKEN + ) + + if result.returncode != 0: + raise ClaudeCodeError( + f"Claude Code CLI returned non-zero exit code: {result.returncode}", + returncode=result.returncode, + stderr=result.stderr, + ) + + return result.stdout + + except subprocess.TimeoutExpired: + raise ClaudeCodeError(f"Claude Code CLI timed out after {timeout} seconds") + except FileNotFoundError: + raise ClaudeCodeError(f"Claude Code CLI executable not found: {cli_path}") + except ClaudeCodeError: + raise # Re-raise our own exceptions as-is + except Exception as e: + raise ClaudeCodeError(f"Failed to invoke Claude Code CLI: {str(e)}") + + +def claude_code_cluster( + leaf_nodes: List[str], + components: Dict[str, Node], + config: Any, + current_module_tree: Dict[str, Any] = None, + current_module_name: Optional[str] = None, +) -> Dict[str, Any]: + """ + Cluster code components into modules using Claude Code CLI. + + Args: + leaf_nodes: List of component IDs to cluster + components: Dictionary mapping component IDs to Node objects + config: Configuration object with claude_code_path and timeout settings + current_module_tree: Current module tree for context (optional) + current_module_name: Name of current module being subdivided (optional) + + Returns: + Dictionary representing the module tree with grouped components + + Raises: + ClaudeCodeError: If clustering fails + """ + if current_module_tree is None: + current_module_tree = {} + + # Format the potential core components for the prompt + potential_core_components, _ = format_potential_core_components(leaf_nodes, components) + + # Build the clustering prompt + if current_module_tree == {}: + prompt = CLUSTER_REPO_PROMPT.format(potential_core_components=potential_core_components) + else: + # Format the module tree for context + lines = [] + + def _format_tree(tree: Dict[str, Any], indent: int = 0): + for key, value in tree.items(): + if key == current_module_name: + lines.append(f"{' ' * indent}{key} (current module)") + else: + lines.append(f"{' ' * indent}{key}") + lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value.get('components', []))}") + children = value.get("children", {}) + if isinstance(children, dict) and len(children) > 0: + lines.append(f"{' ' * (indent + 1)} Children:") + _format_tree(children, indent + 2) + + _format_tree(current_module_tree, 0) + formatted_module_tree = "\n".join(lines) + + prompt = CLUSTER_MODULE_PROMPT.format( + potential_core_components=potential_core_components, + module_tree=formatted_module_tree, + module_name=current_module_name, + ) + + # Get timeout and path from config + timeout = getattr(config, "claude_code_timeout", DEFAULT_CLAUDE_CODE_TIMEOUT) + claude_path = getattr(config, "claude_code_path", None) + + # Invoke Claude Code CLI + logger.info("Invoking Claude Code CLI for module clustering...") + response = _invoke_claude_code(prompt, timeout=timeout, claude_code_path=claude_path) + + # Parse the response - expect JSON wrapped in tags + try: + if "" not in response or "" not in response: + logger.error(f"Invalid Claude Code response format - missing component tags: {response[:200]}...") + return {} + + response_content = response.split("")[1].split("")[0] + module_tree = eval(response_content.strip()) + + if not isinstance(module_tree, dict): + logger.error(f"Invalid module tree format - expected dict, got {type(module_tree)}") + return {} + + # Normalize module tree: ensure each module has 'children' key for compatibility + for module_name, module_info in module_tree.items(): + if "children" not in module_info: + module_info["children"] = {} + + return module_tree + + except Exception as e: + logger.error(f"Failed to parse Claude Code clustering response: {e}") + logger.error(f"Response: {response[:500]}...") + return {} + + +def claude_code_generate_docs( + module_name: str, + core_component_ids: List[str], + components: Dict[str, Node], + module_tree: Dict[str, Any], + config: Any, + output_path: str, +) -> str: + """ + Generate documentation for a module using Claude Code CLI. + + Args: + module_name: Name of the module to document + core_component_ids: List of component IDs in this module + components: Dictionary mapping component IDs to Node objects + module_tree: The full module tree for context + config: Configuration object + output_path: Path where documentation should be saved + + Returns: + The generated markdown documentation + + Raises: + ClaudeCodeError: If documentation generation fails + """ + # Determine if this is a complex or leaf module + is_complex = is_complex_module(components, core_component_ids) + + # Get custom instructions from config + custom_instructions = None + if hasattr(config, "get_prompt_addition"): + custom_instructions = config.get_prompt_addition() + + # Build system prompt based on complexity + if is_complex: + system_prompt = format_system_prompt(module_name, custom_instructions) + else: + system_prompt = format_leaf_system_prompt(module_name, custom_instructions) + + # Build user prompt with module context + user_prompt = format_user_prompt( + module_name=module_name, + core_component_ids=core_component_ids, + components=components, + module_tree=module_tree, + ) + + # Combine into full prompt for Claude Code CLI + # Claude Code handles system/user separation internally, so we combine them + full_prompt = f"""You are a documentation assistant. Follow these instructions: + +{system_prompt} + +--- + +Now complete this task: + +{user_prompt} + +IMPORTANT: Output ONLY the markdown documentation content. Do not wrap in code blocks. +Save the documentation to: {output_path}/{module_name}.md +""" + + # Get timeout and path from config + timeout = getattr(config, "claude_code_timeout", DEFAULT_CLAUDE_CODE_TIMEOUT) + claude_path = getattr(config, "claude_code_path", None) + repo_path = getattr(config, "repo_path", None) + + # Invoke Claude Code CLI + logger.info(f"Invoking Claude Code CLI for documentation: {module_name}") + response = _invoke_claude_code( + full_prompt, + timeout=timeout, + claude_code_path=claude_path, + working_dir=repo_path, + ) + + return response + + +def claude_code_generate_overview( + prompt: str, + config: Any, +) -> str: + """ + Generate repository or module overview using Claude Code CLI. + + Args: + prompt: The formatted overview prompt (REPO_OVERVIEW_PROMPT or MODULE_OVERVIEW_PROMPT) + config: Configuration object + + Returns: + The raw response from Claude Code CLI + + Raises: + ClaudeCodeError: If overview generation fails + """ + # Get timeout and path from config + timeout = getattr(config, "claude_code_timeout", DEFAULT_CLAUDE_CODE_TIMEOUT) + claude_path = getattr(config, "claude_code_path", None) + repo_path = getattr(config, "repo_path", None) + + logger.info("Invoking Claude Code CLI for overview generation...") + response = _invoke_claude_code( + prompt, + timeout=timeout, + claude_code_path=claude_path, + working_dir=repo_path, + ) + + return response diff --git a/codewiki/src/be/dependency_analyzer/ast_parser.py b/codewiki/src/be/dependency_analyzer/ast_parser.py index 3323ed7..fc3c5db 100644 --- a/codewiki/src/be/dependency_analyzer/ast_parser.py +++ b/codewiki/src/be/dependency_analyzer/ast_parser.py @@ -18,16 +18,18 @@ class DependencyParser: """Parser for extracting code components from multi-language repositories.""" - def __init__(self, repo_path: str, include_patterns: List[str] = None, exclude_patterns: List[str] = None): + def __init__(self, repo_path: str, include_patterns: List[str] = None, exclude_patterns: List[str] = None, target_file: str = None): """ Initialize the dependency parser. - + Args: repo_path: Path to the repository include_patterns: File patterns to include (e.g., ["*.cs", "*.py"]) exclude_patterns: File/directory patterns to exclude (e.g., ["*Tests*"]) + target_file: Optional path to a single file for focused documentation """ self.repo_path = os.path.abspath(repo_path) + self.target_file = target_file self.components: Dict[str, Node] = {} self.modules: Set[str] = set() self.include_patterns = include_patterns @@ -49,14 +51,14 @@ def parse_repository(self, filtered_folders: List[str] = None) -> Dict[str, Node include_patterns=self.include_patterns, exclude_patterns=self.exclude_patterns ) - + call_graph_result = self.analysis_service._analyze_call_graph( - structure_result["file_tree"], + structure_result["file_tree"], self.repo_path ) - + self._build_components_from_analysis(call_graph_result) - + logger.debug(f"Found {len(self.components)} components across {len(self.modules)} modules") return self.components diff --git a/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py b/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py index f638f4c..2f86542 100644 --- a/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py +++ b/codewiki/src/be/dependency_analyzer/dependency_graphs_builder.py @@ -11,10 +11,10 @@ class DependencyGraphBuilder: """Handles dependency analysis and graph building.""" - + def __init__(self, config: Config): self.config = config - + def build_dependency_graph(self) -> tuple[Dict[str, Any], List[str]]: """ Build and save dependency graph, returning components and leaf nodes. @@ -44,7 +44,8 @@ def build_dependency_graph(self) -> tuple[Dict[str, Any], List[str]]: parser = DependencyParser( self.config.repo_path, include_patterns=include_patterns, - exclude_patterns=exclude_patterns + exclude_patterns=exclude_patterns, + target_file=self.config.target_file ) filtered_folders = None @@ -99,4 +100,4 @@ def build_dependency_graph(self) -> tuple[Dict[str, Any], List[str]]: else: logger.warning(f"Leaf node {leaf_node} not found in components, removing it") - return components, keep_leaf_nodes \ No newline at end of file + return components, keep_leaf_nodes diff --git a/codewiki/src/be/documentation_generator.py b/codewiki/src/be/documentation_generator.py index 261be61..5069a88 100644 --- a/codewiki/src/be/documentation_generator.py +++ b/codewiki/src/be/documentation_generator.py @@ -158,9 +158,16 @@ async def generate_module_documentation(self, components: Dict[str, Any], leaf_n # Process the module if self.is_leaf_module(module_info): logger.info(f"📄 Processing leaf module: {module_key}") - final_module_tree = await self.agent_orchestrator.process_module( - module_name, components, module_info["components"], module_path, working_dir - ) + if self.config.use_claude_code: + # Use Claude Code CLI for documentation generation + final_module_tree = await self._process_module_with_claude_code( + module_name, components, module_info["components"], + module_tree, working_dir + ) + else: + final_module_tree = await self.agent_orchestrator.process_module( + module_name, components, module_info["components"], module_path, working_dir + ) else: logger.info(f"📁 Processing parent module: {module_key}") final_module_tree = await self.generate_parent_module_docs( @@ -182,9 +189,15 @@ async def generate_module_documentation(self, components: Dict[str, Any], leaf_n else: logger.info(f"Processing whole repo because repo can fit in the context window") repo_name = os.path.basename(os.path.normpath(self.config.repo_path)) - final_module_tree = await self.agent_orchestrator.process_module( - repo_name, components, leaf_nodes, [], working_dir - ) + if self.config.use_claude_code: + # Use Claude Code CLI for documentation generation + final_module_tree = await self._process_module_with_claude_code( + repo_name, components, leaf_nodes, module_tree, working_dir + ) + else: + final_module_tree = await self.agent_orchestrator.process_module( + repo_name, components, leaf_nodes, [], working_dir + ) # save final_module_tree to module_tree.json file_manager.save_json(final_module_tree, os.path.join(working_dir, MODULE_TREE_FILENAME)) @@ -231,21 +244,80 @@ async def generate_parent_module_docs(self, module_path: List[str], ) try: - parent_docs = call_llm(prompt, self.config) - + # Use Claude Code CLI if configured, otherwise use direct LLM call + if self.config.use_claude_code: + from codewiki.src.be.claude_code_adapter import claude_code_generate_overview + parent_docs = claude_code_generate_overview(prompt, self.config) + else: + parent_docs = call_llm(prompt, self.config) + # Parse and save parent documentation - parent_content = parent_docs.split("")[1].split("")[0].strip() - # parent_content = prompt + if "" in parent_docs and "" in parent_docs: + parent_content = parent_docs.split("")[1].split("")[0].strip() + else: + # Claude Code might return the content directly without tags + parent_content = parent_docs.strip() file_manager.save_text(parent_content, parent_docs_path) - + logger.debug(f"Successfully generated parent documentation for: {module_name}") return module_tree - + except Exception as e: logger.error(f"Error generating parent documentation for {module_name}: {str(e)}") logger.error(f"Traceback: {traceback.format_exc()}") raise - + + async def _process_module_with_claude_code( + self, + module_name: str, + components: Dict[str, Any], + core_component_ids: List[str], + module_tree: Dict[str, Any], + working_dir: str, + ) -> Dict[str, Any]: + """ + Process a module using Claude Code CLI for documentation generation. + + Args: + module_name: Name of the module + components: All code components + core_component_ids: Component IDs in this module + module_tree: The full module tree + working_dir: Output directory for documentation + + Returns: + Updated module tree + """ + from codewiki.src.be.claude_code_adapter import claude_code_generate_docs + + # Check if docs already exist + docs_path = os.path.join(working_dir, f"{module_name}.md") + if os.path.exists(docs_path): + logger.info(f"✓ Module docs already exists at {docs_path}") + return module_tree + + try: + # Generate documentation using Claude Code CLI + doc_content = claude_code_generate_docs( + module_name=module_name, + core_component_ids=core_component_ids, + components=components, + module_tree=module_tree, + config=self.config, + output_path=working_dir, + ) + + # Save the generated documentation + file_manager.save_text(doc_content, docs_path) + logger.info(f"✓ Generated documentation for {module_name}") + + return module_tree + + except Exception as e: + logger.error(f"Claude Code documentation generation failed for {module_name}: {e}") + logger.error(f"Traceback: {traceback.format_exc()}") + raise + async def run(self) -> None: """Run the complete documentation generation process using dynamic programming.""" try: diff --git a/codewiki/src/be/gemini_code_adapter.py b/codewiki/src/be/gemini_code_adapter.py new file mode 100644 index 0000000..9486308 --- /dev/null +++ b/codewiki/src/be/gemini_code_adapter.py @@ -0,0 +1,407 @@ +""" +Gemini CLI adapter for CodeWiki. + +This module provides functions to invoke Gemini CLI as an alternative LLM backend +for module clustering and documentation generation. + +## Usage + +The adapter invokes Gemini CLI in non-interactive mode with YOLO mode: + gemini -y "prompt" + +Or via stdin for large prompts: + echo "prompt" | gemini -y + +## Prompt Size Limits + +Gemini 2.0 has a context window of approximately: +- **~1,000,000 tokens** (1M context window) + +The adapter validates prompt size before sending and raises `GeminiCodeError` +if the prompt exceeds the configurable `max_prompt_tokens` limit (default: 900K tokens). + +## Error Handling + +- `GeminiCodeError`: Raised for all CLI failures (not found, timeout, exit code != 0, prompt too large) +- Timeout: Configurable via `gemini_code_timeout` in config (default: 600s) +""" + +import json +import logging +import shutil +import subprocess +from typing import Any, Dict, List, Optional + +from codewiki.src.be.dependency_analyzer.models.core import Node +from codewiki.src.be.prompt_template import ( + CLUSTER_REPO_PROMPT, + CLUSTER_MODULE_PROMPT, + format_user_prompt, + format_system_prompt, + format_leaf_system_prompt, +) +from codewiki.src.be.cluster_modules import format_potential_core_components +from codewiki.src.be.utils import is_complex_module + +logger = logging.getLogger(__name__) + +# Default timeout for Gemini CLI (seconds) - longer due to larger context +DEFAULT_GEMINI_CODE_TIMEOUT = 600 + +# Default max prompt size (in estimated tokens) +# Gemini 2.0 has ~1M token context window +# Setting to 900K to leave room for response +DEFAULT_MAX_PROMPT_TOKENS = 900_000 + + +class GeminiCodeError(Exception): + """Exception raised when Gemini CLI invocation fails.""" + + def __init__(self, message: str, returncode: Optional[int] = None, stderr: Optional[str] = None): + super().__init__(message) + self.returncode = returncode + self.stderr = stderr + + +def _find_gemini_cli(config_path: Optional[str] = None) -> str: + """ + Find the Gemini CLI executable. + + Args: + config_path: Optional configured path to gemini CLI + + Returns: + Path to gemini CLI executable + + Raises: + GeminiCodeError: If CLI cannot be found + """ + if config_path: + if shutil.which(config_path): + return config_path + raise GeminiCodeError(f"Gemini CLI not found at configured path: {config_path}") + + # Try default 'gemini' in PATH + gemini_path = shutil.which("gemini") + if gemini_path: + return gemini_path + + raise GeminiCodeError( + "Gemini CLI not found in PATH. " + "Please install Gemini CLI: npm install -g @anthropic-ai/gemini-cli " + "or configure the path with 'codewiki config set --gemini-code-path '" + ) + + +def _invoke_gemini_code( + prompt: str, + timeout: int = DEFAULT_GEMINI_CODE_TIMEOUT, + gemini_code_path: Optional[str] = None, + working_dir: Optional[str] = None, + max_prompt_tokens: int = DEFAULT_MAX_PROMPT_TOKENS, +) -> str: + """ + Invoke Gemini CLI with a prompt and return the output. + + Args: + prompt: The prompt to send to Gemini + timeout: Timeout in seconds (default: 600) + gemini_code_path: Optional path to gemini CLI executable + working_dir: Optional working directory for the subprocess + max_prompt_tokens: Maximum allowed prompt size in estimated tokens (default: 900K) + + Returns: + The stdout output from Gemini CLI + + Raises: + GeminiCodeError: If CLI invocation fails or prompt exceeds size limit + """ + # Calculate prompt size metrics first + prompt_chars = len(prompt) + prompt_tokens_estimate = prompt_chars // 4 # Rough estimate: ~4 chars per token + + logger.info(f"Prompt size: {prompt_chars:,} chars (~{prompt_tokens_estimate:,} tokens estimated)") + + # Check prompt size limit before invoking CLI + if prompt_tokens_estimate > max_prompt_tokens: + raise GeminiCodeError( + f"Prompt too large: ~{prompt_tokens_estimate:,} tokens estimated, " + f"max allowed: {max_prompt_tokens:,} tokens. " + f"Consider reducing the scope or splitting the request." + ) + + # Warn if prompt is approaching the limit (over 66% of max) + if prompt_tokens_estimate > max_prompt_tokens * 0.66: + logger.warning( + f"Large prompt: ~{prompt_tokens_estimate:,} tokens " + f"({prompt_tokens_estimate * 100 // max_prompt_tokens}% of {max_prompt_tokens:,} limit)" + ) + + cli_path = _find_gemini_cli(gemini_code_path) + + # Build command - use -y for YOLO mode (auto-approve all actions) + # Use --output-format text for clean output + # Prompt is passed via stdin to handle large prompts + cmd = [cli_path, "-y", "--output-format", "text"] + + logger.info(f"Invoking Gemini CLI: {cli_path}") + + try: + result = subprocess.run( + cmd, + input=prompt, # Pass prompt via stdin + capture_output=True, + text=True, + timeout=timeout, + cwd=working_dir, + ) + + if result.returncode != 0: + raise GeminiCodeError( + f"Gemini CLI returned non-zero exit code: {result.returncode}", + returncode=result.returncode, + stderr=result.stderr, + ) + + # Filter out Gemini CLI log lines from stdout + output_lines = result.stdout.split('\n') + filtered_lines = [] + skip_prefixes = ( + 'YOLO mode', + 'Loaded cached', + 'Loading extension', + 'Initializing', + 'Connected to', + ) + for line in output_lines: + if not any(line.startswith(prefix) for prefix in skip_prefixes): + filtered_lines.append(line) + + return '\n'.join(filtered_lines) + + except subprocess.TimeoutExpired: + raise GeminiCodeError(f"Gemini CLI timed out after {timeout} seconds") + except FileNotFoundError: + raise GeminiCodeError(f"Gemini CLI executable not found: {cli_path}") + except GeminiCodeError: + raise # Re-raise our own exceptions as-is + except Exception as e: + raise GeminiCodeError(f"Failed to invoke Gemini CLI: {str(e)}") + + +def gemini_code_cluster( + leaf_nodes: List[str], + components: Dict[str, Node], + config: Any, + current_module_tree: Dict[str, Any] = None, + current_module_name: Optional[str] = None, +) -> Dict[str, Any]: + """ + Cluster code components into modules using Gemini CLI. + + Args: + leaf_nodes: List of component IDs to cluster + components: Dictionary mapping component IDs to Node objects + config: Configuration object with gemini_code_path and timeout settings + current_module_tree: Current module tree for context (optional) + current_module_name: Name of current module being subdivided (optional) + + Returns: + Dictionary representing the module tree with grouped components + + Raises: + GeminiCodeError: If clustering fails + """ + if current_module_tree is None: + current_module_tree = {} + + # Format the potential core components for the prompt + potential_core_components, _ = format_potential_core_components(leaf_nodes, components) + + # Build the clustering prompt + if current_module_tree == {}: + prompt = CLUSTER_REPO_PROMPT.format(potential_core_components=potential_core_components) + else: + # Format the module tree for context + lines = [] + + def _format_tree(tree: Dict[str, Any], indent: int = 0): + for key, value in tree.items(): + if key == current_module_name: + lines.append(f"{' ' * indent}{key} (current module)") + else: + lines.append(f"{' ' * indent}{key}") + lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value.get('components', []))}") + children = value.get("children", {}) + if isinstance(children, dict) and len(children) > 0: + lines.append(f"{' ' * (indent + 1)} Children:") + _format_tree(children, indent + 2) + + _format_tree(current_module_tree, 0) + formatted_module_tree = "\n".join(lines) + + prompt = CLUSTER_MODULE_PROMPT.format( + potential_core_components=potential_core_components, + module_tree=formatted_module_tree, + module_name=current_module_name, + ) + + # Get timeout and path from config + timeout = getattr(config, "gemini_code_timeout", DEFAULT_GEMINI_CODE_TIMEOUT) + gemini_path = getattr(config, "gemini_code_path", None) + + # Invoke Gemini CLI + logger.info("Invoking Gemini CLI for module clustering...") + response = _invoke_gemini_code(prompt, timeout=timeout, gemini_code_path=gemini_path) + + # Parse the response - expect JSON wrapped in tags + # Be more flexible: try to find JSON even if closing tag is missing + try: + if "" in response: + # Extract content after opening tag + response_content = response.split("")[1] + # Try to find closing tag, but if not present, use the rest + if "" in response_content: + response_content = response_content.split("")[0] + # Clean up any trailing text after the JSON + response_content = response_content.strip() + else: + # Try to find raw JSON in response + logger.warning("No tag found, attempting to parse raw JSON...") + response_content = response.strip() + + # Try to parse as JSON first, fall back to eval + import json + try: + module_tree = json.loads(response_content) + except json.JSONDecodeError: + # Try eval as fallback (for Python dict literals) + module_tree = eval(response_content) + + if not isinstance(module_tree, dict): + logger.error(f"Invalid module tree format - expected dict, got {type(module_tree)}") + return {} + + # Normalize module tree: ensure each module has 'children' key for compatibility + for module_name, module_info in module_tree.items(): + if "children" not in module_info: + module_info["children"] = {} + + return module_tree + + except Exception as e: + logger.error(f"Failed to parse Gemini clustering response: {e}") + logger.error(f"Response: {response[:500]}...") + return {} + + +def gemini_code_generate_docs( + module_name: str, + core_component_ids: List[str], + components: Dict[str, Node], + module_tree: Dict[str, Any], + config: Any, + output_path: str, +) -> str: + """ + Generate documentation for a module using Gemini CLI. + + Args: + module_name: Name of the module to document + core_component_ids: List of component IDs in this module + components: Dictionary mapping component IDs to Node objects + module_tree: The full module tree for context + config: Configuration object + output_path: Path where documentation should be saved + + Returns: + The generated markdown documentation + + Raises: + GeminiCodeError: If documentation generation fails + """ + # Determine if this is a complex or leaf module + is_complex = is_complex_module(components, core_component_ids) + + # Get custom instructions from config + custom_instructions = None + if hasattr(config, "get_prompt_addition"): + custom_instructions = config.get_prompt_addition() + + # Build system prompt based on complexity + if is_complex: + system_prompt = format_system_prompt(module_name, custom_instructions) + else: + system_prompt = format_leaf_system_prompt(module_name, custom_instructions) + + # Build user prompt with module context + user_prompt = format_user_prompt( + module_name=module_name, + core_component_ids=core_component_ids, + components=components, + module_tree=module_tree, + ) + + # Combine into full prompt for Gemini CLI + full_prompt = f"""You are a documentation assistant. Follow these instructions: + +{system_prompt} + +--- + +Now complete this task: + +{user_prompt} + +IMPORTANT: Output ONLY the markdown documentation content. Do not wrap in code blocks. +Save the documentation to: {output_path}/{module_name}.md +""" + + # Get timeout and path from config + timeout = getattr(config, "gemini_code_timeout", DEFAULT_GEMINI_CODE_TIMEOUT) + gemini_path = getattr(config, "gemini_code_path", None) + repo_path = getattr(config, "repo_path", None) + + # Invoke Gemini CLI + logger.info(f"Invoking Gemini CLI for documentation: {module_name}") + response = _invoke_gemini_code( + full_prompt, + timeout=timeout, + gemini_code_path=gemini_path, + working_dir=repo_path, + ) + + return response + + +def gemini_code_generate_overview( + prompt: str, + config: Any, +) -> str: + """ + Generate repository or module overview using Gemini CLI. + + Args: + prompt: The formatted overview prompt (REPO_OVERVIEW_PROMPT or MODULE_OVERVIEW_PROMPT) + config: Configuration object + + Returns: + The raw response from Gemini CLI + + Raises: + GeminiCodeError: If overview generation fails + """ + # Get timeout and path from config + timeout = getattr(config, "gemini_code_timeout", DEFAULT_GEMINI_CODE_TIMEOUT) + gemini_path = getattr(config, "gemini_code_path", None) + repo_path = getattr(config, "repo_path", None) + + logger.info("Invoking Gemini CLI for overview generation...") + response = _invoke_gemini_code( + prompt, + timeout=timeout, + gemini_code_path=gemini_path, + working_dir=repo_path, + ) + + return response diff --git a/codewiki/src/be/llm_services.py b/codewiki/src/be/llm_services.py index 0de9843..d3453ae 100644 --- a/codewiki/src/be/llm_services.py +++ b/codewiki/src/be/llm_services.py @@ -59,28 +59,33 @@ def call_llm( prompt: str, config: Config, model: str = None, - temperature: float = 0.0 + temperature: float = None ) -> str: """ Call LLM with the given prompt. - + Args: prompt: The prompt to send config: Configuration containing LLM settings model: Model name (defaults to config.main_model) - temperature: Temperature setting - + temperature: Temperature setting (None = don't send, for models that don't support it) + Returns: LLM response text """ if model is None: model = config.main_model - + client = create_openai_client(config) - response = client.chat.completions.create( - model=model, - messages=[{"role": "user", "content": prompt}], - temperature=temperature, - max_tokens=config.max_tokens - ) + + # Build request kwargs - only include temperature if specified + kwargs = { + "model": model, + "messages": [{"role": "user", "content": prompt}], + "max_tokens": config.max_tokens + } + if temperature is not None: + kwargs["temperature"] = temperature + + response = client.chat.completions.create(**kwargs) return response.choices[0].message.content \ No newline at end of file diff --git a/codewiki/src/be/prompt_template.py b/codewiki/src/be/prompt_template.py index d37d5b6..d6710c5 100644 --- a/codewiki/src/be/prompt_template.py +++ b/codewiki/src/be/prompt_template.py @@ -263,11 +263,12 @@ def _format_module_tree(module_tree: dict[str, any], indent: int = 0): lines.append(f"{' ' * indent}{key} (current module)") else: lines.append(f"{' ' * indent}{key}") - - lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value['components'])}") - if isinstance(value["children"], dict) and len(value["children"]) > 0: + + lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value.get('components', []))}") + children = value.get("children", {}) + if isinstance(children, dict) and len(children) > 0: lines.append(f"{' ' * (indent + 1)} Children:") - _format_module_tree(value["children"], indent + 2) + _format_module_tree(children, indent + 2) _format_module_tree(module_tree, 0) formatted_module_tree = "\n".join(lines) @@ -323,16 +324,16 @@ def _format_module_tree(module_tree: dict[str, any], indent: int = 0): lines.append(f"{' ' * indent}{key} (current module)") else: lines.append(f"{' ' * indent}{key}") - - lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value['components'])}") - if ("children" in value) and isinstance(value["children"], dict) and len(value["children"]) > 0: + + lines.append(f"{' ' * (indent + 1)} Core components: {', '.join(value.get('components', []))}") + children = value.get("children", {}) + if isinstance(children, dict) and len(children) > 0: lines.append(f"{' ' * (indent + 1)} Children:") - _format_module_tree(value["children"], indent + 2) - + _format_module_tree(children, indent + 2) + _format_module_tree(module_tree, 0) formatted_module_tree = "\n".join(lines) - if module_tree == {}: return CLUSTER_REPO_PROMPT.format(potential_core_components=potential_core_components) else: diff --git a/codewiki/src/config.py b/codewiki/src/config.py index 420d1ea..85e1487 100644 --- a/codewiki/src/config.py +++ b/codewiki/src/config.py @@ -63,6 +63,16 @@ class Config: max_token_per_leaf_module: int = DEFAULT_MAX_TOKEN_PER_LEAF_MODULE # Agent instructions for customization agent_instructions: Optional[Dict[str, Any]] = None + # Single-file documentation mode + target_file: Optional[str] = None + # Claude Code CLI integration + use_claude_code: bool = False + claude_code_path: Optional[str] = None + claude_code_timeout: int = 900 + # Gemini CLI integration (larger context window) + use_gemini_code: bool = False + gemini_code_path: Optional[str] = None + gemini_code_timeout: int = 600 @property def include_patterns(self) -> Optional[List[str]]: @@ -155,15 +165,23 @@ def from_cli( main_model: str, cluster_model: str, fallback_model: str = FALLBACK_MODEL_1, + max_tokens: int = DEFAULT_MAX_TOKENS, max_token_per_module: int = DEFAULT_MAX_TOKEN_PER_MODULE, max_token_per_leaf_module: int = DEFAULT_MAX_TOKEN_PER_LEAF_MODULE, max_depth: int = MAX_DEPTH, - agent_instructions: Optional[Dict[str, Any]] = None + agent_instructions: Optional[Dict[str, Any]] = None, + target_file: Optional[str] = None, + use_claude_code: bool = False, + claude_code_path: Optional[str] = None, + claude_code_timeout: int = 900, + use_gemini_code: bool = False, + gemini_code_path: Optional[str] = None, + gemini_code_timeout: int = 600, ) -> 'Config': """ Create configuration for CLI context. - + Args: repo_path: Repository path output_dir: Output directory for generated docs @@ -177,13 +195,20 @@ def from_cli( max_token_per_leaf_module: Maximum tokens per leaf module max_depth: Maximum depth for hierarchical decomposition agent_instructions: Custom agent instructions dict - + target_file: Optional path to single file for focused documentation + use_claude_code: Whether to use Claude Code CLI as LLM backend + claude_code_path: Optional path to claude CLI executable + claude_code_timeout: Timeout for Claude Code CLI in seconds + use_gemini_code: Whether to use Gemini CLI as LLM backend (larger context) + gemini_code_path: Optional path to gemini CLI executable + gemini_code_timeout: Timeout for Gemini CLI in seconds + Returns: Config instance """ repo_name = os.path.basename(os.path.normpath(repo_path)) base_output_dir = os.path.join(output_dir, "temp") - + return cls( repo_path=repo_path, output_dir=base_output_dir, @@ -198,5 +223,12 @@ def from_cli( max_tokens=max_tokens, max_token_per_module=max_token_per_module, max_token_per_leaf_module=max_token_per_leaf_module, - agent_instructions=agent_instructions - ) \ No newline at end of file + agent_instructions=agent_instructions, + target_file=target_file, + use_claude_code=use_claude_code, + claude_code_path=claude_code_path, + claude_code_timeout=claude_code_timeout, + use_gemini_code=use_gemini_code, + gemini_code_path=gemini_code_path, + gemini_code_timeout=gemini_code_timeout, + ) diff --git a/pyproject.toml b/pyproject.toml index 00c3e01..07a7cc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,9 @@ dev = [ "mypy>=1.5.0", "ruff>=0.1.0", ] +poc = [ + "the-edge-agent[llm]>=0.9.69", +] [project.scripts] codewiki = "codewiki.cli.main:cli" @@ -120,5 +123,5 @@ testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] -addopts = "-v --cov=codewiki --cov-report=term-missing" +addopts = "-v"