Files
coding-agent-gitea/gitea/tools/coding_tools.py
T
michael 0461c6c7ab Add .gitignore and pyproject.toml (#1)
### Findings and Changes

#### Changes:
- **Added **: Included a standard Python  to avoid tracking unnecessary files (e.g., , , ).
- **Added **: Prepared the project for better dependency management.
- **Enhanced Gitea Tools**:
    - Implemented  in .
    - Implemented  (via ) in .

#### Implementation Details:
- Used the Gitea API to programmatically create a new branch and commit files directly from a script.
- Verified that the NAME:
   tea - command line tool to interact with Gitea

USAGE:
   tea [global options] [command [command options]]

VERSION:
   Version: 0.14.1  golang: 1.26.3  go-sdk: v0.25.1

DESCRIPTION:
   tea is a productivity helper for Gitea. It can be used to manage most entities on
   one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.

   tea tries to make use of context provided by the repository in $PWD if available.
   tea works best in a upstream/fork workflow, when the local main branch tracks the
   upstream repo. tea assumes that local git state is published on the remote before
   doing operations with tea.    Configuration is persisted in $XDG_CONFIG_HOME/tea.

COMMANDS:
   help, h  Shows a list of commands or help for one command

   ENTITIES:
     issues, issue, i                  List, create and update issues
     pulls, pull, pr                   Manage and checkout pull requests
     labels, label                     Manage issue labels
     milestones, milestone, ms         List and create milestones
     releases, release, r              Manage releases
     times, time, t                    Operate on tracked times of a repository's issues & pulls
     organizations, organization, org  List, create, delete organizations
     repos, repo                       Manage repositories
     branches, branch, b               Consult branches
     actions, action                   Manage repository actions
     webhooks, webhook, hooks, hook    Manage webhooks
     comment, c                        Add a comment to an issue / pr

   HELPERS:
     open, o                         Open something of the repository in web browser
     notifications, notification, n  Show notifications
     clone, C                        Clone a repository locally
     api                             Make an authenticated API request

   MISCELLANEOUS:
     whoami    Show current logged in user
     admin, a  Operations requiring admin access on the Gitea instance

   SETUP:
     logins, login      Log in to a Gitea server
     logout             Log out from a Gitea server
     ssh-keys, ssh-key  Manage SSH public keys

GLOBAL OPTIONS:
   --debug, --vvv  Enable debug mode
   --help, -h      show help
   --version, -v   print the version CLI can be used for automated PR creation.
- Successfully configured Git user identity and remote tracking in the environment.

---------

Co-authored-by: Michael <michael@example.com>
Reviewed-on: #1
2026-06-28 18:38:28 +02:00

209 lines
8.8 KiB
Python

"""Tools for a coding agent to interact with the filesystem and environment."""
import os
import subprocess
from typing import Any
class CodingTools:
"""Tools for a coding agent to interact with the filesystem and environment."""
def __init__(self, repo_path: str | None = None) -> None:
self.repo_path: str = repo_path or os.getcwd()
def get_working_directory(self) -> str:
"""Get the absolute path of the current local repository workspace directory."""
return self.repo_path
def _resolve_path(self, path: str) -> str:
"""Resolve a path relative to self.repo_path."""
if os.path.isabs(path):
return path
return os.path.abspath(os.path.join(self.repo_path, path))
def list_files(self, path: str = ".") -> str:
"""List all files and directories at the given path (relative to repo root or absolute)."""
resolved: str = self._resolve_path(path)
try:
items: list[str] = os.listdir(resolved)
return "\n".join(items)
except Exception as e:
return f"Error listing files: {str(e)}"
def read_file(self, path: str, offset: int = 1, limit: int = 250) -> str:
"""Read lines from a file, starting at line offset (1-indexed), up to limit lines. Default limit is 250 lines to prevent token bloat. Use 'offset' to scroll/page through larger files."""
resolved: str = self._resolve_path(path)
try:
with open(resolved, 'r', encoding='utf-8', errors='replace') as f:
lines: list[str] = f.readlines()
start_line: int = offset - 1
end_line: int = offset + limit - 1
content_lines: list[str] = lines[start_line:end_line]
if not content_lines:
return "File is empty or offset out of bounds."
formatted_lines: list[str] = [f"{i + 1}: {line}" for i, line in enumerate(content_lines, start=start_line)]
return "\n".join(formatted_lines)
except Exception as e:
return f"Error reading file: {str(e)}"
def write_file(self, path: str, content: str) -> str:
"""Write content to a file, creating directories as needed."""
resolved: str = self._resolve_path(path)
try:
os.makedirs(os.path.dirname(os.path.abspath(resolved)), exist_ok=True)
with open(resolved, 'w', encoding='utf-8') as f:
f.write(content)
return f"File {path} written successfully."
except Exception as e:
return f"Error writing file: {str(e)}"
def edit_file(self, path: str, old_content: str, new_content: str) -> str:
"""Replace occurrences of old_content with new_content in the file."""
resolved: str = self._resolve_path(path)
try:
with open(resolved, 'r', encoding='utf-8', errors='replace') as f:
content: str = f.read()
if old_content not in content:
return f"Error: The specified old content was not found in {path}."
new_content_full: str = content.replace(old_content, new_content)
with open(resolved, 'w', encoding='utf-8') as f:
f.write(new_content_full)
return f"File {path} edited successfully."
except Exception as e:
return f"Error editing file: {str(e)}"
def _parse_verification_commands(self) -> list[str]:
agents_md: str = os.path.join(self.repo_path, "AGENTS.md")
if not os.path.exists(agents_md):
return []
try:
with open(agents_md, "r", encoding="utf-8") as f:
content = f.read()
except Exception:
return []
commands: list[str] = []
in_verification_section = False
in_code_block = False
current_block: list[str] = []
for line in content.splitlines():
line_lower = line.strip().lower()
if line.startswith("#"):
if "verification" in line_lower or "test" in line_lower:
in_verification_section = True
else:
in_verification_section = False
continue
if in_verification_section:
if line.strip().startswith("```"):
if in_code_block:
in_code_block = False
full_cmd = "\n".join(current_block).strip()
if full_cmd:
for cmd in full_cmd.splitlines():
if cmd.strip() and not cmd.strip().startswith("#"):
commands.append(cmd.strip())
current_block = []
else:
in_code_block = True
elif in_code_block:
current_block.append(line)
return commands
def run_verification(self) -> tuple[bool, str]:
commands = self._parse_verification_commands()
if not commands:
return True, "No verification commands found in AGENTS.md."
log_output = []
for cmd in commands:
log_output.append(f"Running: {cmd}")
try:
res = subprocess.run(
cmd, shell=True, cwd=self.repo_path,
capture_output=True, text=True, timeout=120
)
if res.returncode != 0:
log_output.append(
f"Command '{cmd}' failed with exit code {res.returncode}:\n"
f"Stdout:\n{res.stdout}\n"
f"Stderr:\n{res.stderr}"
)
return False, "\n".join(log_output)
log_output.append(res.stdout or "Success")
except subprocess.TimeoutExpired:
log_output.append(f"Command '{cmd}' timed out after 120 seconds.")
return False, "\n".join(log_output)
except Exception as e:
log_output.append(f"Failed to execute command '{cmd}': {e}")
return False, "\n".join(log_output)
return True, "\n".join(log_output)
def run_command(self, command: str, timeout: int = 120) -> str:
"""Execute a shell command in the repository workspace and return stdout and stderr output. Args: command, timeout (default 120 seconds)."""
if "tea pr create" in command:
success, log_msg = self.run_verification()
if not success:
return f"Verification failed! You cannot create a pull request because the tests/checks are failing:\n\n{log_msg}\n\nPlease fix the issues and try again."
try:
process: subprocess.Popen[str] = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.repo_path
)
stdout: str
stderr: str
try:
stdout, stderr = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
return f"Command timed out after {timeout} seconds.\n--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}"
output: str = ""
if stdout:
output += f"--- STDOUT ---\n{stdout}"
if stderr:
output += f"\n--- STDERR ---\n{stderr}"
if process.returncode != 0:
return f"Command failed with exit code {process.returncode}:\n{output}"
return output if output else "Command executed successfully (no output)."
except Exception as e:
return f"Error running command: {str(e)}"
def grep_search(self, pattern: str, path: str = ".") -> str:
"""Search for pattern in files under path using grep (case-insensitive)."""
resolved: str = self._resolve_path(path)
try:
command: str = f"grep -ri '{pattern}' {resolved}"
process: subprocess.Popen[str] = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.repo_path
)
stdout: str
stderr: str
try:
stdout, stderr = process.communicate(timeout=30)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
return f"Grep search timed out after 30 seconds.\nStdout: {stdout}\nStderr: {stderr}"
if process.returncode != 0 and not stdout:
return f"No matches found for '{pattern}'."
output: str = stdout
if stderr:
output += f"\nError: {stderr}"
return output
except Exception as e:
return f"Error during grep search: {str(e)}"