diff --git a/cecli/tools/explore_symbols.py b/cecli/tools/explore_symbols.py index 8fa061486db..930ede70ce1 100644 --- a/cecli/tools/explore_symbols.py +++ b/cecli/tools/explore_symbols.py @@ -5,12 +5,16 @@ from cecli.tools.utils.helpers import ToolError from cecli.tools.utils.output import color_markers, tool_footer, tool_header +cwd = os.getcwd() + try: import cymbal CYMBAL_AVAILABLE = True except ImportError: CYMBAL_AVAILABLE = False +finally: + os.chdir(cwd) class Tool(BaseTool): @@ -95,14 +99,12 @@ def execute(cls, coder, queries, **kwargs): repo_path = getattr(coder, "root", ".") try: - # If we can't get a db_path or it doesn't exist, index it. - if not os.path.exists(c.db_path): - c.index(repo_path) + # Always index to ensure we have the latest data + c.index(repo_path) except Exception as e: error_msg = f"Failed to index repository: {str(e)}" coder.io.tool_error(error_msg) return f"Error: {error_msg}" - all_results = [] all_failed_queries = [] total_successful_queries = 0 @@ -117,11 +119,8 @@ def execute(cls, coder, queries, **kwargs): results = c.search(symbol, limit=limit) all_results.append(cls._format_search_results(results, symbol)) elif action == "investigate": - # Parse symbol for file hint format: {file}:{symbol} or {package}.{symbol} symbol_name = symbol file_hint = "" - - # Check for file:symbol format (e.g., "config.go:Config") if ":" in symbol: parts = symbol.split(":", 1) if len(parts) == 2: @@ -135,7 +134,6 @@ def execute(cls, coder, queries, **kwargs): ) except Exception as e: if "multiple matches" in str(e).lower(): - # Fallback to search to show locations results = c.search(symbol_name, limit=10) locations = "\n".join( [f"- {r['file']}:{r['start_line']}" for r in results] @@ -178,6 +176,9 @@ def execute(cls, coder, queries, **kwargs): except Exception as e: coder.io.tool_error(f"Error in ExploreSymbols: {str(e)}") return f"Error: {str(e)}" + finally: + if "c" in locals(): + c.close() @classmethod def _format_search_results(cls, results, symbol): @@ -187,14 +188,18 @@ def _format_search_results(cls, results, symbol): formatted = [f"Found {len(results)} symbols matching '{symbol}':"] for i, result in enumerate(results[:15], 1): - # Extract symbol attributes (adjust based on actual cymbal result structure) - # Extract symbol attributes from dictionary name = result.get("name", "Unknown") kind = result.get("kind", "unknown") - file = result.get("file", "Unknown") + file = result.get("rel_path") or result.get("file", "Unknown") start_line = result.get("start_line", 0) + signature = result.get("signature", "") + parent = result.get("parent") - formatted.append(f"{i}. {name} ({kind}) at {file}:{start_line}") + location = f"{file}:{start_line}" + if parent: + location = f"{location} (in {parent})" + + formatted.append(f"{i}. {name}{signature} ({kind}) at {location}") if len(results) > 15: formatted.append(f"... and {len(results) - 15} more results") @@ -207,17 +212,33 @@ def _format_investigation_results(cls, investigation, symbol): if not investigation: return f"No information found for symbol '{symbol}'" + # Handle nested structure if present + if "results" in investigation and "result" in investigation["results"]: + investigation = investigation["results"]["result"] + formatted = [f"Investigation of symbol '{symbol}':"] # Extract definition information definition = investigation.get("symbol") if definition: def_name = definition.get("name", symbol) - def_file = definition.get("file", "Unknown") + def_file = definition.get("rel_path") or definition.get("file", "Unknown") def_line = definition.get("start_line", 0) def_kind = definition.get("kind", "unknown") - formatted.append(f"Definition: {def_name} ({def_kind}) at {def_file}:{def_line}") - + def_sig = definition.get("signature", "") + formatted.append( + f"Definition: {def_name}{def_sig} ({def_kind}) at {def_file}:{def_line}" + ) + + # Source code snippet + source = investigation.get("source") + if source: + formatted.append("\nSource Code:") + formatted.append("```python") + formatted.append(source.strip()) + formatted.append("```") + + # References references = investigation.get("refs", []) ref_count = len(references) if references else 0 formatted.append(f"\nReferences found: {ref_count}") @@ -225,13 +246,26 @@ def _format_investigation_results(cls, investigation, symbol): if references and ref_count > 0: formatted.append("Top references:") for i, ref in enumerate(references[:10], 1): - ref_file = ref.get("file", "Unknown") + ref_file = ref.get("rel_path") or ref.get("file", "Unknown") ref_line = ref.get("line", 0) formatted.append(f"{i}. {ref_file}:{ref_line}") if ref_count > 10: formatted.append(f"... and {ref_count - 10} more references") + # Impact / Callers + impact = investigation.get("impact", []) + if impact: + formatted.append("\nImpact (Callers):") + for i, imp in enumerate(impact[:10], 1): + imp_file = imp.get("rel_path") or imp.get("file", "Unknown") + imp_line = imp.get("line", 0) + imp_caller = imp.get("caller", "unknown") + formatted.append(f"{i}. {imp_caller} at {imp_file}:{imp_line}") + + if len(impact) > 10: + formatted.append(f"... and {len(impact) - 10} more callers") + return "\n".join(formatted) @classmethod @@ -242,11 +276,15 @@ def _format_reference_results(cls, references, symbol): formatted = [f"Found {len(references)} references to '{symbol}':"] for i, ref in enumerate(references[:15], 1): - # Extract reference attributes from dictionary - file = ref.get("file", "Unknown") + file = ref.get("rel_path") or ref.get("file", "Unknown") line = ref.get("line", 0) + context = ref.get("context", []) formatted.append(f"{i}. {file}:{line}") + if context: + formatted.append(" Context:") + for line_text in context: + formatted.append(f" {line_text.strip()}") if len(references) > 15: formatted.append(f"... and {len(references) - 15} more references") @@ -266,8 +304,6 @@ def format_output(cls, coder, mcp_server, tool_response): # Output header tool_header(coder=coder, mcp_server=mcp_server, tool_response=tool_response) - - # Output each query with the requested format queries = params.get("queries", []) if queries: coder.io.tool_output("") diff --git a/cecli/tools/ls.py b/cecli/tools/ls.py index 9a9ed276340..06236289aef 100644 --- a/cecli/tools/ls.py +++ b/cecli/tools/ls.py @@ -11,67 +11,78 @@ class Tool(BaseTool): SCHEMA = { "type": "function", "function": { - "name": "Ls", - "description": "List files in a directory.", + "name": "ls", + "description": "List files in a directory. Paths are relative to the project root.", "parameters": { "type": "object", "properties": { - "directory": { + "path": { "type": "string", - "description": "The directory to list.", - }, + "description": ( + "The path of the directory to list, relative to the project root. " + "Defaults to the project root." + ), + "default": ".", + } }, - "required": ["directory"], + "required": [], }, }, } @classmethod - def execute(cls, coder, dir_path=None, directory=None, **kwargs): - # Handle both positional and keyword arguments for backward compatibility - if dir_path is None and directory is not None: - dir_path = directory - elif dir_path is None: - return "Error: Missing directory parameter" + def execute(cls, coder, path=None, **kwargs): """ List files in directory and optionally add some to context. This provides information about the structure of the codebase, similar to how a developer would explore directories. """ + # Handle both positional and keyword arguments for backward compatibility + dir_path = path or "." + try: - # Make the path relative to root if it's absolute - if dir_path.startswith("/"): - rel_dir = os.path.relpath(dir_path, coder.root) - else: - rel_dir = dir_path + # Create an absolute path from the provided relative path + abs_path = os.path.abspath(os.path.join(coder.root, dir_path)) - # Get absolute path - abs_dir = coder.abs_root_path(rel_dir) + # Security check: ensure the resolved path is within the project root + if not abs_path.startswith(os.path.abspath(coder.root)): + coder.io.tool_error( + f"Error: Path '{dir_path}' attempts to access files outside the project root." + ) + return "Error: Path is outside the project root." # Check if path exists - if not os.path.exists(abs_dir): - coder.io.tool_output(f"⚠️ Directory '{dir_path}' not found") + if not os.path.exists(abs_path): + coder.io.tool_output(f"⚠️ Path '{dir_path}' not found") return "Directory not found" # Get directory contents contents = [] - try: - with os.scandir(abs_dir) as entries: - for entry in entries: - if entry.is_file() and not entry.name.startswith("."): - rel_path = os.path.join(rel_dir, entry.name) - contents.append(rel_path) - except NotADirectoryError: - # If it's a file, just return the file - contents = [rel_dir] + if os.path.isdir(abs_path): + # It's a directory, list its contents + try: + with os.scandir(abs_path) as entries: + for entry in entries: + if entry.is_file() and not entry.name.startswith("."): + rel_path = os.path.relpath(entry.path, coder.root) + contents.append(rel_path) + except OSError as e: + coder.io.tool_error(f"Error listing directory '{dir_path}': {e}") + return f"Error: {e}" + elif os.path.isfile(abs_path): + # It's a file, just return its relative path + contents.append(os.path.relpath(abs_path, coder.root)) if contents: coder.io.tool_output(f"📋 Listed {len(contents)} file(s) in '{dir_path}'") - if len(contents) > 10: - return f"Found {len(contents)} files: {', '.join(contents[:10])}..." + sorted_contents = sorted(contents) + if len(sorted_contents) > 10: + return ( + f"Found {len(sorted_contents)} files: {', '.join(sorted_contents[:10])}..." + ) else: - return f"Found {len(contents)} files: {', '.join(contents)}" + return f"Found {len(sorted_contents)} files: {', '.join(sorted_contents)}" else: coder.io.tool_output(f"📋 No files found in '{dir_path}'") return "No files found in directory" @@ -93,10 +104,10 @@ def format_output(cls, coder, mcp_server, tool_response): tool_header(coder=coder, mcp_server=mcp_server, tool_response=tool_response) # Output the directory parameter with the requested format - directory = params.get("directory", "") + directory = params.get("path", "") if directory: # Format as "ls: • directory" - formatted_query = f"{color_start}directory:{color_end} {directory}" + formatted_query = f"{color_start}path:{color_end} {directory}" coder.io.tool_output(formatted_query) coder.io.tool_output("") diff --git a/requirements.txt b/requirements.txt index da3062df4b7..5b81c0115bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -274,7 +274,7 @@ ptyprocess==0.7.0 # via # -c requirements/common-constraints.txt # pexpect -py-cymbal==0.1.6 +py-cymbal==0.1.24 # via # -c requirements/common-constraints.txt # -r requirements/requirements.in diff --git a/requirements/common-constraints.txt b/requirements/common-constraints.txt index aa5ecdc63fe..c1c84b6ab26 100644 --- a/requirements/common-constraints.txt +++ b/requirements/common-constraints.txt @@ -345,7 +345,7 @@ psutil==7.1.3 # via -r requirements/requirements.in ptyprocess==0.7.0 # via pexpect -py-cymbal==0.1.6 +py-cymbal==0.1.24 # via -r requirements/requirements.in pycodestyle==2.14.0 # via flake8 diff --git a/requirements/requirements.in b/requirements/requirements.in index 46ff0b74403..1c13b060815 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -32,7 +32,7 @@ textual>=6.0.0 tomlkit>=0.14.0 truststore xxhash>=3.6.0 -py-cymbal>=0.1.6 +py-cymbal>=0.1.24 # Replaced networkx with rustworkx for better performance in repomap rustworkx>=0.15.0