diff --git a/src/mcp_client.py b/src/mcp_client.py index f68f5224..144b1478 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -576,6 +576,28 @@ def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = F except Exception as e: return f"ERROR editing '{path}': {e}" +def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str: + """ + Returns the git diff for a file or directory. + base_rev: The base revision (default: HEAD) + head_rev: The head revision (optional) + """ + p, err = _resolve_and_check(path) + if err: + return err + assert p is not None + cmd = ["git", "diff", base_rev] + if head_rev: + cmd.append(head_rev) + cmd.extend(["--", str(p)]) + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True, encoding="utf-8") + return result.stdout if result.stdout else "(no changes)" + except subprocess.CalledProcessError as e: + return f"ERROR running git diff: {e.stderr}" + except Exception as e: + return f"ERROR: {e}" + def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]: """Helper to find an AST node by name (Class, Function, or Variable). Supports dot notation.""" parts = name.split(".") @@ -602,6 +624,8 @@ def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]: current = found return current +#region: Python AST + def py_get_symbol_info(path: str, name: str) -> tuple[str, int] | str: """ @@ -806,30 +830,6 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str: except Exception as e: return f"ERROR updating variable '{name}' in '{path}': {e}" -def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str: - """ - - - Returns the git diff for a file or directory. - base_rev: The base revision (default: HEAD) - head_rev: The head revision (optional) - """ - p, err = _resolve_and_check(path) - if err: - return err - assert p is not None - cmd = ["git", "diff", base_rev] - if head_rev: - cmd.append(head_rev) - cmd.extend(["--", str(p)]) - try: - result = subprocess.run(cmd, capture_output=True, text=True, check=True, encoding="utf-8") - return result.stdout if result.stdout else "(no changes)" - except subprocess.CalledProcessError as e: - return f"ERROR running git diff: {e.stderr}" - except Exception as e: - return f"ERROR: {e}" - def py_find_usages(path: str, name: str) -> str: """Finds exact string matches of a symbol in a given file or directory.""" p, err = _resolve_and_check(path) @@ -964,38 +964,6 @@ def py_get_docstring(path: str, name: str) -> str: except Exception as e: return f"ERROR getting docstring for '{name}': {e}" -def get_tree(path: str, max_depth: int = 2) -> str: - """Returns a directory structure up to a max depth.""" - p, err = _resolve_and_check(path) - if err: return err - assert p is not None - if not p.is_dir(): return f"ERROR: not a directory: {path}" - try: - m_depth = max_depth - - def _build_tree(dir_path: Path, current_depth: int, prefix: str = "") -> list[str]: - if current_depth > m_depth: return [] - lines = [] - try: - entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower())) - except PermissionError: - return [] - # Filter - entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")] - for i, entry in enumerate(entries): - is_last = (i == len(entries) - 1) - connector = "└── " if is_last else "├── " - lines.append(f"{prefix}{connector}{entry.name}") - if entry.is_dir(): - extension = " " if is_last else "│ " - lines.extend(_build_tree(entry, current_depth + 1, prefix + extension)) - return lines - tree_lines = [f"{p.name}/"] + _build_tree(p, 1) - return "\n".join(tree_lines) - except Exception as e: - return f"ERROR generating tree for '{path}': {e}" - # ------------------------------------------------------------------ web tools - def derive_code_path(target: str, max_depth: int = 5) -> str: """Recursively traces the execution path of a specific function or method.""" from src.file_cache import ASTParser @@ -1056,6 +1024,42 @@ def derive_code_path(target: str, max_depth: int = 5) -> str: trace(symbol_name, found_path, found_code, 0, "") return "\n".join(output) +#endregion Python AST + +def get_tree(path: str, max_depth: int = 2) -> str: + """Returns a directory structure up to a max depth.""" + p, err = _resolve_and_check(path) + if err: return err + assert p is not None + if not p.is_dir(): return f"ERROR: not a directory: {path}" + try: + m_depth = max_depth + + def _build_tree(dir_path: Path, current_depth: int, prefix: str = "") -> list[str]: + if current_depth > m_depth: return [] + lines = [] + try: + entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower())) + except PermissionError: + return [] + # Filter + entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")] + for i, entry in enumerate(entries): + is_last = (i == len(entries) - 1) + connector = "└── " if is_last else "├── " + lines.append(f"{prefix}{connector}{entry.name}") + if entry.is_dir(): + extension = " " if is_last else "│ " + lines.extend(_build_tree(entry, current_depth + 1, prefix + extension)) + return lines + tree_lines = [f"{p.name}/"] + _build_tree(p, 1) + return "\n".join(tree_lines) + except Exception as e: + return f"ERROR generating tree for '{path}': {e}" + # ------------------------------------------------------------------ web tools + +#region: Web + class _DDGParser(HTMLParser): def __init__(self) -> None: super().__init__() @@ -1161,6 +1165,8 @@ def fetch_url(url: str) -> str: return full_text except Exception as e: return f"ERROR fetching URL '{url}': {e}" + +#endregion: Web def get_ui_performance() -> str: """