diff --git a/codemcp/main.py b/codemcp/main.py index 6d1636f..9ca9b34 100644 --- a/codemcp/main.py +++ b/codemcp/main.py @@ -18,7 +18,7 @@ from .git_query import get_current_commit_hash from .tools.chmod import chmod from .tools.edit_file import edit_file_content -from .tools.glob import MAX_RESULTS, glob_files +from .tools.glob import glob_files from .tools.grep import grep_files from .tools.init_project import init_project from .tools.ls import ls_directory @@ -182,7 +182,7 @@ async def codemcp( if path is None: raise ValueError("path is required for ReadFile subtool") - result = await read_file_content(path, offset, limit, chat_id) + result = await read_file_content(path, offset, limit, chat_id, commit_hash) return result if subtool == "WriteFile": @@ -193,7 +193,9 @@ async def codemcp( if chat_id is None: raise ValueError("chat_id is required for WriteFile subtool") - result = await write_file_content(path, content, description, chat_id) + result = await write_file_content( + path, content, description, chat_id, commit_hash + ) return result if subtool == "EditFile": @@ -210,12 +212,12 @@ async def codemcp( raise ValueError("chat_id is required for EditFile subtool") # Accept either old_string or old_str (prefer old_string if both are provided) - old_content = old_string or old_str or "" + old_content = old_string or old_str # Accept either new_string or new_str (prefer new_string if both are provided) - new_content = new_string or new_str or "" + new_content = new_string or new_str result = await edit_file_content( - path, old_content, new_content, None, description, chat_id + path, old_content, new_content, None, description, chat_id, commit_hash ) return result @@ -223,7 +225,7 @@ async def codemcp( if path is None: raise ValueError("path is required for LS subtool") - result = await ls_directory(path, chat_id) + result = await ls_directory(path, chat_id, commit_hash) return result if subtool == "InitProject": @@ -255,7 +257,7 @@ async def codemcp( if chat_id is None: raise ValueError("chat_id is required for RunCommand subtool") - result = await run_command(path, command, arguments, chat_id) + result = await run_command(path, command, arguments, chat_id, commit_hash) return result if subtool == "Grep": @@ -265,7 +267,9 @@ async def codemcp( raise ValueError("path is required for Grep subtool") try: - grep_result = await grep_files(pattern, path, include, chat_id) + grep_result = await grep_files( + pattern, path, include, chat_id, commit_hash + ) result_string = grep_result.get( "resultForAssistant", f"Found {grep_result.get('numFiles', 0)} file(s)", @@ -286,9 +290,10 @@ async def codemcp( glob_result = await glob_files( pattern, path, - limit=limit if limit is not None else MAX_RESULTS, - offset=offset if offset is not None else 0, - chat_id=chat_id, + limit, + offset, + chat_id, + commit_hash, ) result_string = glob_result.get( "resultForAssistant", @@ -308,7 +313,7 @@ async def codemcp( if chat_id is None: raise ValueError("chat_id is required for RM subtool") - result = await rm_file(path, description, chat_id) + result = await rm_file(path, description, chat_id, commit_hash) return result if subtool == "MV": @@ -325,17 +330,16 @@ async def codemcp( if chat_id is None: raise ValueError("chat_id is required for MV subtool") - result = await mv_file(source_path, target_path, description, chat_id) + result = await mv_file( + source_path, target_path, description, chat_id, commit_hash + ) return result if subtool == "Think": if thought is None: raise ValueError("thought is required for Think subtool") - result = await think(thought, chat_id) - # Think doesn't need a path, but we might have one in the provided parameters - if path: - return result + result = await think(thought, chat_id, commit_hash) return result if subtool == "Chmod": @@ -346,11 +350,12 @@ async def codemcp( if chat_id is None: raise ValueError("chat_id is required for Chmod subtool") - chmod_result = await chmod(path, mode, chat_id) + chmod_result = await chmod(path, mode, chat_id, commit_hash) result_string = chmod_result.get( "resultForAssistant", "Chmod operation completed" ) return result_string + except Exception: logging.error("Exception", exc_info=True) raise diff --git a/codemcp/tools/chmod.py b/codemcp/tools/chmod.py index 5a0dd16..cd6d5b6 100644 --- a/codemcp/tools/chmod.py +++ b/codemcp/tools/chmod.py @@ -32,6 +32,7 @@ async def chmod( path: str, mode: str, # Changed from Literal["a+x", "a-x"] to str to handle validation internally chat_id: str | None = None, + commit_hash: str | None = None, ) -> dict[str, Any]: """Change file permissions using chmod. @@ -39,10 +40,14 @@ async def chmod( path: The absolute path to the file to modify mode: The chmod mode to apply, only "a+x" and "a-x" are supported chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A dictionary with chmod output """ + # Set default values + chat_id = "" if chat_id is None else chat_id + if not path: raise ValueError("File path must be provided") @@ -113,7 +118,7 @@ async def chmod( success, commit_message = await commit_changes( directory, description, - chat_id if chat_id is not None else "", + chat_id, ) if not success: diff --git a/codemcp/tools/commit_utils.py b/codemcp/tools/commit_utils.py index c5108cb..a98b3e0 100644 --- a/codemcp/tools/commit_utils.py +++ b/codemcp/tools/commit_utils.py @@ -11,19 +11,24 @@ ] -async def append_commit_hash(result: str, path: str | None) -> Tuple[str, str | None]: +async def append_commit_hash( + result: str, path: str | None, commit_hash: str | None = None +) -> Tuple[str, str | None]: """Get the current Git commit hash and append it to the result string. Args: result: The original result string to append to path: Path to the Git repository (if available) + commit_hash: Optional Git commit hash to use instead of fetching the current one Returns: A tuple containing: - The result string with the commit hash appended - The current commit hash if available, None otherwise """ - current_hash = None + # If commit_hash is provided, use it directly + if commit_hash: + return f"{result}\n\nCurrent commit hash: {commit_hash}", commit_hash if path is None: return result, None @@ -38,4 +43,4 @@ async def append_commit_hash(result: str, path: str | None) -> Tuple[str, str | except Exception as e: logging.warning(f"Failed to get current commit hash: {e}", exc_info=True) - return result, current_hash + return result, None diff --git a/codemcp/tools/edit_file.py b/codemcp/tools/edit_file.py index dcabdf9..2e101eb 100644 --- a/codemcp/tools/edit_file.py +++ b/codemcp/tools/edit_file.py @@ -746,11 +746,12 @@ def debug_string_comparison( async def edit_file_content( file_path: str, - old_string: str, - new_string: str, + old_string: str | None = None, + new_string: str | None = None, read_file_timestamps: dict[str, float] | None = None, - description: str = "", - chat_id: str = "", + description: str | None = None, + chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Edit a file by replacing old_string with new_string. @@ -761,11 +762,12 @@ async def edit_file_content( Args: file_path: The absolute path to the file to edit - old_string: The text to replace + old_string: The text to replace (use empty string for new file creation) new_string: The new text to replace old_string with read_file_timestamps: Dictionary mapping file paths to timestamps when they were last read description: Short description of the change chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A success message @@ -776,6 +778,12 @@ async def edit_file_content( Files must be tracked in the git repository before they can be modified. """ + # Set default values + old_string = "" if old_string is None else old_string + new_string = "" if new_string is None else new_string + description = "" if description is None else description + chat_id = "" if chat_id is None else chat_id + # Normalize the file path full_file_path = normalize_file_path(file_path) @@ -840,7 +848,7 @@ async def edit_file_content( result = f"Successfully created {full_file_path}{git_message}" # Append commit hash - result, _ = await append_commit_hash(result, full_file_path) + result, _ = await append_commit_hash(result, full_file_path, commit_hash) return result # Check if file exists @@ -936,5 +944,5 @@ async def edit_file_content( result = f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{format_message}{git_message}" # Append commit hash - result, _ = await append_commit_hash(result, full_file_path) + result, _ = await append_commit_hash(result, full_file_path, commit_hash) return result diff --git a/codemcp/tools/glob.py b/codemcp/tools/glob.py index 46b3c82..5b6c644 100644 --- a/codemcp/tools/glob.py +++ b/codemcp/tools/glob.py @@ -146,9 +146,10 @@ def render_result_for_assistant(output: Dict[str, Any]) -> str: async def glob_files( pattern: str, path: str | None = None, - limit: int = MAX_RESULTS, - offset: int = 0, + limit: int | None = None, + offset: int | None = None, chat_id: str | None = None, + commit_hash: str | None = None, ) -> Dict[str, Any]: """Search for files matching a glob pattern. @@ -158,15 +159,21 @@ async def glob_files( limit: Maximum number of results to return offset: Number of results to skip (for pagination) chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A dictionary with matched files + """ try: # Use current directory if path is not provided directory = path or os.getcwd() normalized_path = normalize_file_path(directory) + # Set default values for limit and offset + limit = limit or MAX_RESULTS + offset = offset or 0 + # Execute glob with options for pagination options = {"limit": limit, "offset": offset} result = await glob(pattern, normalized_path, options) @@ -176,7 +183,7 @@ async def glob_files( # Append commit hash formatted_result, _ = await append_commit_hash( - formatted_result, normalized_path + formatted_result, normalized_path, commit_hash ) result["resultForAssistant"] = formatted_result diff --git a/codemcp/tools/grep.py b/codemcp/tools/grep.py index 2aeff7b..4050fa9 100644 --- a/codemcp/tools/grep.py +++ b/codemcp/tools/grep.py @@ -161,6 +161,7 @@ async def grep_files( path: str | None = None, include: str | None = None, chat_id: str | None = None, + commit_hash: str | None = None, ) -> Dict[str, Any]: """Search for a pattern in files within a directory or in a specific file. @@ -169,14 +170,21 @@ async def grep_files( path: The directory or file to search in (must be in a git repository) include: Optional file pattern to filter the search chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A dictionary with matched files """ try: + # Set default values + chat_id = "" if chat_id is None else chat_id + + # Default to current directory if path is not provided + path = "." if path is None else path + # Normalize the path - normalized_path = normalize_file_path(path) if path else None + normalized_path = normalize_file_path(path) # Execute git grep matched_files = await git_grep(pattern, normalized_path, include) @@ -201,7 +209,7 @@ async def grep_files( # Append commit hash if normalized_path: result_for_assistant, _ = await append_commit_hash( - result_for_assistant, normalized_path + result_for_assistant, normalized_path, commit_hash ) output["resultForAssistant"] = result_for_assistant diff --git a/codemcp/tools/ls.py b/codemcp/tools/ls.py index 1896988..a88f151 100644 --- a/codemcp/tools/ls.py +++ b/codemcp/tools/ls.py @@ -23,17 +23,23 @@ TRUNCATED_MESSAGE = f"There are more than {MAX_FILES} files in the directory. Use more specific paths to explore nested directories. The first {MAX_FILES} files and directories are included below:\n\n" -async def ls_directory(directory_path: str, chat_id: str | None = None) -> str: +async def ls_directory( + directory_path: str, chat_id: str | None = None, commit_hash: str | None = None +) -> str: """List the contents of a directory. Args: directory_path: The absolute path to the directory to list chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A formatted string representation of the directory contents """ + # Set default values + chat_id = "" if chat_id is None else chat_id + # Normalize the directory path full_directory_path = normalize_file_path(directory_path) @@ -69,7 +75,7 @@ async def ls_directory(directory_path: str, chat_id: str | None = None) -> str: output = f"{TRUNCATED_MESSAGE}{tree_output}" # Append commit hash - result, _ = await append_commit_hash(output, full_directory_path) + result, _ = await append_commit_hash(output, full_directory_path, commit_hash) return result diff --git a/codemcp/tools/mv.py b/codemcp/tools/mv.py index 72d8404..ef915e6 100644 --- a/codemcp/tools/mv.py +++ b/codemcp/tools/mv.py @@ -17,8 +17,9 @@ async def mv_file( source_path: str, target_path: str, - description: str, - chat_id: str = "", + description: str | None = None, + chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Move a file using git mv. @@ -27,10 +28,15 @@ async def mv_file( target_path: The path to the target location (can be absolute or relative to repository root) description: Short description of why the file is being moved chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A string containing the result of the move operation """ + # Set default values + description = "" if description is None else description + chat_id = "" if chat_id is None else chat_id + # Use the directory from the path as our starting point for source source_path = normalize_file_path(source_path) source_dir_path = ( @@ -137,5 +143,5 @@ async def mv_file( result = f"File was moved from {source_rel_path} to {target_rel_path} but failed to commit: {commit_message}" # Append commit hash - result, _ = await append_commit_hash(result, git_root_resolved) + result, _ = await append_commit_hash(result, git_root_resolved, commit_hash) return result diff --git a/codemcp/tools/read_file.py b/codemcp/tools/read_file.py index 4cc847f..faadfc8 100644 --- a/codemcp/tools/read_file.py +++ b/codemcp/tools/read_file.py @@ -23,6 +23,7 @@ async def read_file_content( offset: int | None = None, limit: int | None = None, chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Read a file's content with optional offset and limit. @@ -31,11 +32,15 @@ async def read_file_content( offset: The line number to start reading from (1-indexed) limit: The number of lines to read chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: The file content as a string """ + # Set default values + chat_id = "" if chat_id is None else chat_id + # Normalize the file path full_file_path = normalize_file_path(file_path) @@ -107,5 +112,5 @@ async def read_file_content( content += get_applicable_rules_content(repo_root, full_file_path) # Append commit hash - result, _ = await append_commit_hash(content, full_file_path) + result, _ = await append_commit_hash(content, full_file_path, commit_hash) return result diff --git a/codemcp/tools/rm.py b/codemcp/tools/rm.py index 8009ede..757d6ed 100644 --- a/codemcp/tools/rm.py +++ b/codemcp/tools/rm.py @@ -16,8 +16,9 @@ async def rm_file( path: str, - description: str, - chat_id: str = "", + description: str | None = None, + chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Remove a file using git rm. @@ -25,10 +26,15 @@ async def rm_file( path: The path to the file to remove (can be absolute or relative to repository root) description: Short description of why the file is being removed chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A string containing the result of the removal operation """ + # Set default values + description = "" if description is None else description + chat_id = "" if chat_id is None else chat_id + # Use the directory from the path as our starting point file_path = normalize_file_path(path) dir_path = os.path.dirname(file_path) if os.path.dirname(file_path) else "." @@ -98,5 +104,5 @@ async def rm_file( result = f"File {rel_path} was removed but failed to commit: {commit_message}" # Append commit hash - result, _ = await append_commit_hash(result, git_root_resolved) + result, _ = await append_commit_hash(result, git_root_resolved, commit_hash) return result diff --git a/codemcp/tools/run_command.py b/codemcp/tools/run_command.py index fa4e1cc..3127206 100644 --- a/codemcp/tools/run_command.py +++ b/codemcp/tools/run_command.py @@ -15,8 +15,9 @@ async def run_command( project_dir: str, command: str, - arguments: Optional[str | list] = None, - chat_id: str = "", + arguments: Optional[str | list[str]] = None, + chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Run a command that is configured in codemcp.toml. @@ -28,10 +29,14 @@ async def run_command( tokenization (spaces separate arguments, quotes can be used for arguments containing spaces, etc.). If a list, it will be used directly. chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A string containing the result of the command operation """ + # Set default values + chat_id = "" if chat_id is None else chat_id + # Normalize the project directory path project_dir = normalize_file_path(project_dir) @@ -58,5 +63,5 @@ async def run_command( ) # Append commit hash - result, _ = await append_commit_hash(result, project_dir) + result, _ = await append_commit_hash(result, project_dir, commit_hash) return result diff --git a/codemcp/tools/think.py b/codemcp/tools/think.py index 915c602..e90cc6d 100644 --- a/codemcp/tools/think.py +++ b/codemcp/tools/think.py @@ -7,16 +7,22 @@ ] -async def think(thought: str, chat_id: str | None = None) -> str: +async def think( + thought: str, chat_id: str | None = None, commit_hash: str | None = None +) -> str: """Use this tool to think about something without obtaining new information or changing the database. Args: thought: The thought to log chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A confirmation message that the thought was logged """ + # Set default values + chat_id = "" if chat_id is None else chat_id + # Log the thought but don't actually do anything with it logging.info(f"[{chat_id}] Thought: {thought}") diff --git a/codemcp/tools/write_file.py b/codemcp/tools/write_file.py index ce152d0..e923d61 100644 --- a/codemcp/tools/write_file.py +++ b/codemcp/tools/write_file.py @@ -23,8 +23,9 @@ async def write_file_content( file_path: str, content: str | dict | list | None = None, - description: str = "", - chat_id: str = "", + description: str | None = None, + chat_id: str | None = None, + commit_hash: str | None = None, ) -> str: """Write content to a file. @@ -33,6 +34,7 @@ async def write_file_content( content: The content to write to the file. Can be a string, dict, or list (will be converted to JSON) description: Short description of the change chat_id: The unique ID of the current chat session + commit_hash: Optional Git commit hash for version tracking Returns: A success message @@ -43,6 +45,10 @@ async def write_file_content( Files must be tracked in the git repository before they can be modified. """ + # Set default values + description = "" if description is None else description + chat_id = "" if chat_id is None else chat_id + # Normalize the file path file_path = normalize_file_path(file_path) @@ -108,5 +114,5 @@ async def write_file_content( result = f"Successfully wrote to {file_path}{format_message}{git_message}" # Append commit hash - result, _ = await append_commit_hash(result, file_path) + result, _ = await append_commit_hash(result, file_path, commit_hash) return result