chmod.py•4.43 kB
#!/usr/bin/env python3
import os
import stat
from typing import Any
from ..common import normalize_file_path
from ..git import commit_changes
from ..mcp import mcp
from ..shell import run_command
from .commit_utils import append_commit_hash
__all__ = [
"chmod",
"render_result_for_assistant",
"TOOL_NAME_FOR_PROMPT",
"DESCRIPTION",
]
TOOL_NAME_FOR_PROMPT = "Chmod"
DESCRIPTION = """
Changes file permissions using chmod. Unlike standard chmod, this tool only supports
a+x (add executable permission) and a-x (remove executable permission), because these
are the only bits that git knows how to track.
Example:
chmod a+x path/to/file # Makes a file executable by all users
chmod a-x path/to/file # Makes a file non-executable for all users
"""
@mcp.tool()
async def chmod(
path: str,
mode: str,
chat_id: str | None = None,
commit_hash: str | None = None,
) -> str:
"""Changes file permissions using chmod. Unlike standard chmod, this tool only supports
a+x (add executable permission) and a-x (remove executable permission), because these
are the only bits that git knows how to track.
Args:
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 to identify the chat session
commit_hash: Optional Git commit hash for version tracking
Example:
chmod a+x path/to/file # Makes a file executable by all users
chmod a-x path/to/file # Makes a file non-executable for all users
Returns:
A formatted string with the chmod operation result
"""
# Set default values
chat_id = "" if chat_id is None else chat_id
if not path:
raise ValueError("File path must be provided")
# Normalize the file path
absolute_path = normalize_file_path(path)
# Check if file exists
if not os.path.exists(absolute_path):
raise FileNotFoundError(f"The file does not exist: {path}")
# Verify that the mode is supported
if mode not in ["a+x", "a-x"]:
raise ValueError(
f"Unsupported chmod mode: {mode}. Only 'a+x' and 'a-x' are supported."
)
# Get the directory containing the file for git operations
directory = os.path.dirname(absolute_path)
# Check current file permissions
current_mode = os.stat(absolute_path).st_mode
is_executable = bool(current_mode & stat.S_IXUSR)
if mode == "a+x" and is_executable:
message = f"File '{path}' is already executable"
# Append commit hash
message, _ = await append_commit_hash(message, directory, commit_hash)
return message
elif mode == "a-x" and not is_executable:
message = f"File '{path}' is already non-executable"
# Append commit hash
message, _ = await append_commit_hash(message, directory, commit_hash)
return message
# Execute chmod command
cmd = ["chmod", mode, absolute_path]
await run_command(
cmd=cmd,
cwd=directory,
capture_output=True,
text=True,
check=True,
)
# Prepare success message
if mode == "a+x":
description = f"Make '{os.path.basename(absolute_path)}' executable"
action_msg = f"Made file '{path}' executable"
else:
description = (
f"Remove executable permission from '{os.path.basename(absolute_path)}'"
)
action_msg = f"Removed executable permission from file '{path}'"
# Commit the changes
success, commit_message = await commit_changes(
directory,
description,
chat_id,
)
if not success:
raise RuntimeError(f"Failed to commit chmod changes: {commit_message}")
# Prepare result string
result_string = f"{action_msg} and committed changes"
# Format the result for assistant
formatted_result = render_result_for_assistant({"output": result_string})
# Append commit hash
formatted_result, _ = await append_commit_hash(
formatted_result, directory, commit_hash
)
return formatted_result
def render_result_for_assistant(output: dict[str, Any]) -> str:
"""Render the results in a format suitable for the assistant.
Args:
output: The chmod output dictionary
Returns:
A formatted string representation of the results
"""
return output.get("output", "")