#!/usr/bin/env python3
import json
import logging
import os
from ..code_command import run_formatter_without_commit
from ..common import normalize_file_path
from ..file_utils import (
check_file_path_and_permissions,
check_git_tracking_for_existing_file,
write_text_content,
)
from ..git import commit_changes
from ..line_endings import detect_line_endings, detect_repo_line_endings
from ..mcp import mcp
from .commit_utils import append_commit_hash
__all__ = [
"write_file",
]
@mcp.tool()
async def write_file(
path: str,
content: str | dict | list | None = None,
description: str | None = None,
chat_id: str | None = None,
commit_hash: str | None = None,
) -> str:
"""Write a file to the local filesystem. Overwrites the existing file if there is one.
Provide a short description of the change.
Before using this tool:
1. Use the ReadFile tool to understand the file's contents and context
2. Directory Verification (only applicable when creating new files):
- Use the LS tool to verify the parent directory exists and is the correct location
Args:
path: The absolute path to the file to write
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
Note:
This function allows creating new files that don't exist yet.
For existing files, it will reject attempts to write to files that are not tracked by git.
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
path = normalize_file_path(path)
# Normalize content - if content is not a string, serialize it to a string using json.dumps
if content is not None and not isinstance(content, str):
content_str = json.dumps(content)
else:
content_str = content or ""
# Normalize newlines
content_str = (
content_str.replace("\r\n", "\n")
if isinstance(content_str, str)
else content_str
)
# Validate file path and permissions
is_valid, error_message = await check_file_path_and_permissions(path)
if not is_valid:
raise ValueError(error_message)
# Check git tracking for existing files
is_tracked, track_error = await check_git_tracking_for_existing_file(path, chat_id)
if not is_tracked:
raise ValueError(track_error)
# Determine line endings
old_file_exists = os.path.exists(path)
if old_file_exists:
line_endings = await detect_line_endings(path)
else:
line_endings = detect_repo_line_endings(os.path.dirname(path))
# Ensure directory exists for new files
directory = os.path.dirname(path)
os.makedirs(directory, exist_ok=True)
# Write the content with UTF-8 encoding and proper line endings
await write_text_content(path, content_str, "utf-8", line_endings)
# Try to run the formatter on the file
format_message = ""
formatter_success, formatter_output = await run_formatter_without_commit(path)
if formatter_success:
logging.info(f"Auto-formatted {path}")
if formatter_output.strip():
format_message = f"\nAuto-formatted the file"
else:
# Only log warning if there was actually a format command configured but it failed
if not "No format command configured" in formatter_output:
logging.warning(f"Failed to auto-format {path}: {formatter_output}")
# Commit the changes
git_message = ""
success, message = await commit_changes(path, description, chat_id)
if success:
git_message = f"\nChanges committed to git: {description}"
else:
git_message = f"\nFailed to commit changes to git: {message}"
result = f"Successfully wrote to {path}{format_message}{git_message}"
# Append commit hash
result, _ = await append_commit_hash(result, path, commit_hash)
return result