tunacode-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
1
+ """
2
+ Module: tunacode.services.undo_service
3
+
4
+ Provides Git-based undo functionality for TunaCode operations.
5
+ Manages automatic commits and rollback operations.
6
+ """
7
+
8
+ import subprocess
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Optional, Tuple
12
+
13
+ from pydantic_ai.messages import ModelResponse, TextPart
14
+
15
+ from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_DISABLED_NO_GIT,
16
+ UNDO_GIT_TIMEOUT, UNDO_INITIAL_COMMIT)
17
+ from tunacode.core.state import StateManager
18
+ from tunacode.exceptions import GitOperationError
19
+ from tunacode.ui import console as ui
20
+ from tunacode.utils.system import get_session_dir
21
+
22
+
23
+ def is_in_git_project(directory: Optional[Path] = None) -> bool:
24
+ """
25
+ Recursively check if the given directory is inside a git project.
26
+
27
+ Args:
28
+ directory (Path, optional): Directory to check. Defaults to current working directory.
29
+
30
+ Returns:
31
+ bool: True if in a git project, False otherwise
32
+ """
33
+ if directory is None:
34
+ directory = Path.cwd()
35
+
36
+ if (directory / ".git").exists():
37
+ return True
38
+
39
+ if directory == directory.parent:
40
+ return False
41
+
42
+ return is_in_git_project(directory.parent)
43
+
44
+
45
+ def init_undo_system(state_manager: StateManager) -> bool:
46
+ """
47
+ Initialize the undo system by creating a Git repository
48
+ in the ~/.tunacode/sessions/<session-id> directory.
49
+
50
+ Skip initialization if running from home directory or not in a git project.
51
+
52
+ Args:
53
+ state_manager: The StateManager instance.
54
+
55
+ Returns:
56
+ bool: True if the undo system was initialized, False otherwise.
57
+ """
58
+ cwd = Path.cwd()
59
+ home_dir = Path.home()
60
+
61
+ if cwd == home_dir:
62
+ ui.warning(UNDO_DISABLED_HOME)
63
+ return False
64
+
65
+ if not is_in_git_project():
66
+ # Temporarily use sync print for warnings during init
67
+ print("⚠️ Not in a git repository - undo functionality will be limited")
68
+ print("💡 To enable undo functionality, run: git init")
69
+ print(" File operations will still work, but can't be undone")
70
+ return False
71
+
72
+ # Get the session directory path
73
+ session_dir = get_session_dir(state_manager)
74
+ tunacode_git_dir = session_dir / ".git"
75
+
76
+ # Check if already initialized
77
+ if tunacode_git_dir.exists():
78
+ return True
79
+
80
+ # Initialize Git repository
81
+ try:
82
+ subprocess.run(
83
+ ["git", "init", str(session_dir)], capture_output=True, check=True, timeout=5
84
+ )
85
+
86
+ # Make an initial commit
87
+ git_dir_arg = f"--git-dir={tunacode_git_dir}"
88
+
89
+ # Add all files
90
+ subprocess.run(["git", git_dir_arg, "add", "."], capture_output=True, check=True, timeout=5)
91
+
92
+ # Create initial commit
93
+ subprocess.run(
94
+ ["git", git_dir_arg, "commit", "-m", UNDO_INITIAL_COMMIT],
95
+ capture_output=True,
96
+ check=True,
97
+ timeout=5,
98
+ )
99
+
100
+ return True
101
+ except subprocess.TimeoutExpired as e:
102
+ error = GitOperationError(operation="init", message=UNDO_GIT_TIMEOUT, original_error=e)
103
+ ui.warning(str(error))
104
+ return False
105
+ except Exception as e:
106
+ error = GitOperationError(operation="init", message=str(e), original_error=e)
107
+ ui.warning(ERROR_UNDO_INIT.format(e=e))
108
+ return False
109
+
110
+
111
+ def commit_for_undo(
112
+ message_prefix: str = "tunacode", state_manager: Optional[StateManager] = None
113
+ ) -> bool:
114
+ """
115
+ Commit the current state to the undo repository.
116
+
117
+ Args:
118
+ message_prefix (str): Prefix for the commit message.
119
+ state_manager: The StateManager instance.
120
+
121
+ Returns:
122
+ bool: True if the commit was successful, False otherwise.
123
+ """
124
+ # Get the session directory and git dir
125
+ if state_manager is None:
126
+ raise ValueError("state_manager is required for commit_for_undo")
127
+ session_dir = get_session_dir(state_manager)
128
+ tunacode_git_dir = session_dir / ".git"
129
+
130
+ if not tunacode_git_dir.exists():
131
+ return False
132
+
133
+ try:
134
+ git_dir_arg = f"--git-dir={tunacode_git_dir}"
135
+
136
+ # Add all files
137
+ subprocess.run(["git", git_dir_arg, "add", "."], capture_output=True, timeout=5)
138
+
139
+ # Create commit with timestamp
140
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
141
+ commit_message = f"{message_prefix} - {timestamp}"
142
+
143
+ result = subprocess.run(
144
+ ["git", git_dir_arg, "commit", "-m", commit_message],
145
+ capture_output=True,
146
+ text=True,
147
+ timeout=5,
148
+ )
149
+
150
+ # Handle case where there are no changes to commit
151
+ if "nothing to commit" in result.stdout or "nothing to commit" in result.stderr:
152
+ return False
153
+
154
+ return True
155
+ except subprocess.TimeoutExpired as e:
156
+ error = GitOperationError(
157
+ operation="commit", message="Git commit timed out", original_error=e
158
+ )
159
+ ui.warning(str(error))
160
+ return False
161
+ except Exception as e:
162
+ error = GitOperationError(operation="commit", message=str(e), original_error=e)
163
+ ui.warning(f"Error creating undo commit: {e}")
164
+ return False
165
+
166
+
167
+ def perform_undo(state_manager: StateManager) -> Tuple[bool, str]:
168
+ """
169
+ Undo the most recent change by resetting to the previous commit.
170
+ Also adds a system message to the chat history to inform the AI
171
+ that the last changes were undone.
172
+
173
+ Args:
174
+ state_manager: The StateManager instance.
175
+
176
+ Returns:
177
+ tuple: (bool, str) - Success status and message
178
+ """
179
+ # Get the session directory and git dir
180
+ session_dir = get_session_dir(state_manager)
181
+ tunacode_git_dir = session_dir / ".git"
182
+
183
+ if not tunacode_git_dir.exists():
184
+ return False, "Undo system not initialized"
185
+
186
+ try:
187
+ git_dir_arg = f"--git-dir={tunacode_git_dir}"
188
+
189
+ # Get commit log to check if we have commits to undo
190
+ result = subprocess.run(
191
+ ["git", git_dir_arg, "log", "--format=%H", "-n", "2"],
192
+ capture_output=True,
193
+ text=True,
194
+ check=True,
195
+ timeout=5,
196
+ )
197
+
198
+ commits = result.stdout.strip().split("\n")
199
+ if len(commits) < 2:
200
+ return False, "Nothing to undo"
201
+
202
+ # Get the commit message of the commit we're undoing for context
203
+ commit_msg_result = subprocess.run(
204
+ ["git", git_dir_arg, "log", "--format=%B", "-n", "1"],
205
+ capture_output=True,
206
+ text=True,
207
+ check=True,
208
+ timeout=5,
209
+ )
210
+ commit_msg = commit_msg_result.stdout.strip()
211
+
212
+ # Perform reset to previous commit
213
+ subprocess.run(
214
+ ["git", git_dir_arg, "reset", "--hard", "HEAD~1"],
215
+ capture_output=True,
216
+ check=True,
217
+ timeout=5,
218
+ )
219
+
220
+ # Add a system message to the chat history to inform the AI
221
+ # about the undo operation
222
+ state_manager.session.messages.append(
223
+ ModelResponse(
224
+ parts=[
225
+ TextPart(
226
+ content=(
227
+ "The last changes were undone. "
228
+ f"Commit message of undone changes: {commit_msg}"
229
+ )
230
+ )
231
+ ],
232
+ kind="response",
233
+ )
234
+ )
235
+
236
+ return True, "Successfully undid last change"
237
+ except subprocess.TimeoutExpired as e:
238
+ error = GitOperationError(
239
+ operation="reset", message="Undo operation timed out", original_error=e
240
+ )
241
+ return False, str(error)
242
+ except Exception as e:
243
+ error = GitOperationError(operation="reset", message=str(e), original_error=e)
244
+ return False, f"Error performing undo: {e}"
tunacode/setup.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Module: tunacode.setup
3
+
4
+ Package setup and metadata configuration for the TunaCode CLI.
5
+ Provides high-level setup functions for initializing the application and its agents.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ from tunacode.core.setup import (AgentSetup, ConfigSetup, EnvironmentSetup, GitSafetySetup,
11
+ SetupCoordinator, UndoSetup)
12
+ from tunacode.core.state import StateManager
13
+
14
+
15
+ async def setup(run_setup: bool, state_manager: StateManager) -> None:
16
+ """
17
+ Setup TunaCode on startup using the new setup coordinator.
18
+
19
+ Args:
20
+ run_setup (bool): If True, force run the setup process, resetting current config.
21
+ state_manager (StateManager): The state manager instance.
22
+ """
23
+ coordinator = SetupCoordinator(state_manager)
24
+
25
+ # Register setup steps in order
26
+ coordinator.register_step(ConfigSetup(state_manager))
27
+ coordinator.register_step(EnvironmentSetup(state_manager))
28
+ coordinator.register_step(GitSafetySetup(state_manager)) # Run after config/env but before undo
29
+ coordinator.register_step(UndoSetup(state_manager))
30
+
31
+ # Run all setup steps
32
+ await coordinator.run_setup(force_setup=run_setup)
33
+
34
+
35
+ async def setup_agent(agent: Optional[Any], state_manager: StateManager) -> None:
36
+ """
37
+ Setup the agent separately.
38
+
39
+ This is called from other parts of the codebase when an agent needs to be initialized.
40
+
41
+ Args:
42
+ agent: The agent instance to initialize.
43
+ state_manager (StateManager): The state manager instance.
44
+ """
45
+ if agent is not None:
46
+ agent_setup = AgentSetup(state_manager, agent)
47
+ if await agent_setup.should_run():
48
+ await agent_setup.execute()
49
+ if not await agent_setup.validate():
50
+ raise RuntimeError("Agent setup failed validation")
File without changes
tunacode/tools/base.py ADDED
@@ -0,0 +1,244 @@
1
+ """Base tool class for all Sidekick tools.
2
+
3
+ This module provides a base class that implements common patterns
4
+ for all tools including error handling, UI logging, and ModelRetry support.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+
9
+ from pydantic_ai.exceptions import ModelRetry
10
+
11
+ from tunacode.exceptions import FileOperationError, ToolExecutionError
12
+ from tunacode.types import FilePath, ToolName, ToolResult, UILogger
13
+
14
+
15
+ class BaseTool(ABC):
16
+ """Base class for all Sidekick tools providing common functionality."""
17
+
18
+ def __init__(self, ui_logger: UILogger | None = None):
19
+ """Initialize the base tool.
20
+
21
+ Args:
22
+ ui_logger: UI logger instance for displaying messages
23
+ """
24
+ self.ui = ui_logger
25
+
26
+ async def execute(self, *args, **kwargs) -> ToolResult:
27
+ """Execute the tool with error handling and logging.
28
+
29
+ This method wraps the tool-specific logic with:
30
+ - UI logging of the operation
31
+ - Exception handling (except ModelRetry and ToolExecutionError)
32
+ - Consistent error message formatting
33
+
34
+ Returns:
35
+ str: Success message
36
+
37
+ Raises:
38
+ ModelRetry: Re-raised to guide the LLM
39
+ ToolExecutionError: Raised for all other errors with structured information
40
+ """
41
+ try:
42
+ if self.ui:
43
+ await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
44
+ result = await self._execute(*args, **kwargs)
45
+
46
+ # For file operations, try to create a git commit for undo tracking
47
+ if isinstance(self, FileBasedTool):
48
+ await self._commit_for_undo()
49
+
50
+ return result
51
+ except ModelRetry as e:
52
+ # Log as warning and re-raise for pydantic-ai
53
+ if self.ui:
54
+ await self.ui.warning(str(e))
55
+ raise
56
+ except ToolExecutionError:
57
+ # Already properly formatted, just re-raise
58
+ raise
59
+ except Exception as e:
60
+ # Handle any other exceptions
61
+ await self._handle_error(e, *args, **kwargs)
62
+
63
+ @property
64
+ @abstractmethod
65
+ def tool_name(self) -> ToolName:
66
+ """Return the display name for this tool."""
67
+ pass
68
+
69
+ @abstractmethod
70
+ async def _execute(self, *args, **kwargs) -> ToolResult:
71
+ """Implement tool-specific logic here.
72
+
73
+ This method should contain the core functionality of the tool.
74
+
75
+ Returns:
76
+ str: Success message describing what was done
77
+
78
+ Raises:
79
+ ModelRetry: When the LLM needs guidance
80
+ Exception: Any other errors will be caught and handled
81
+ """
82
+ pass
83
+
84
+ async def _handle_error(self, error: Exception, *args, **kwargs) -> ToolResult:
85
+ """Handle errors by logging and raising proper exceptions.
86
+
87
+ Args:
88
+ error: The exception that was raised
89
+ *args, **kwargs: Original arguments for context
90
+
91
+ Raises:
92
+ ToolExecutionError: Always raised with structured error information
93
+ """
94
+ # Format error message for display
95
+ err_msg = f"Error {self._get_error_context(*args, **kwargs)}: {error}"
96
+ if self.ui:
97
+ await self.ui.error(err_msg)
98
+
99
+ # Raise proper exception instead of returning string
100
+ raise ToolExecutionError(tool_name=self.tool_name, message=str(error), original_error=error)
101
+
102
+ def _format_args(self, *args, **kwargs) -> str:
103
+ """Format arguments for display in UI logging.
104
+
105
+ Override this method to customize how arguments are displayed.
106
+
107
+ Returns:
108
+ str: Formatted argument string
109
+ """
110
+ # Collect all arguments
111
+ all_args = []
112
+
113
+ # Add positional arguments
114
+ for arg in args:
115
+ if isinstance(arg, str) and len(arg) > 50:
116
+ # Truncate long strings
117
+ all_args.append(f"'{arg[:47]}...'")
118
+ else:
119
+ all_args.append(repr(arg))
120
+
121
+ # Add keyword arguments
122
+ for key, value in kwargs.items():
123
+ if isinstance(value, str) and len(value) > 50:
124
+ all_args.append(f"{key}='{value[:47]}...'")
125
+ else:
126
+ all_args.append(f"{key}={repr(value)}")
127
+
128
+ return ", ".join(all_args)
129
+
130
+ def _get_error_context(self, *args, **kwargs) -> str:
131
+ """Get context string for error messages.
132
+
133
+ Override this method to provide tool-specific error context.
134
+
135
+ Returns:
136
+ str: Context for the error message
137
+ """
138
+ return f"in {self.tool_name}"
139
+
140
+
141
+ class FileBasedTool(BaseTool):
142
+ """Base class for tools that work with files.
143
+
144
+ Provides common file-related functionality like:
145
+ - Path validation
146
+ - File existence checking
147
+ - Directory creation
148
+ - Encoding handling
149
+ - Git commit for undo tracking
150
+ """
151
+
152
+ async def _commit_for_undo(self) -> None:
153
+ """Create a git commit for undo tracking after file operations.
154
+
155
+ This method gracefully handles cases where git is not available:
156
+ - No git repository: Warns user about limited undo functionality
157
+ - Git command fails: Warns but doesn't break the main operation
158
+ - Any other error: Silently continues (file operation still succeeds)
159
+ """
160
+ try:
161
+ # Import here to avoid circular imports
162
+ from tunacode.services.undo_service import commit_for_undo, is_in_git_project
163
+
164
+ # Check if we're in a git project first
165
+ if not is_in_git_project():
166
+ if self.ui:
167
+ await self.ui.muted("⚠️ No git repository - undo functionality limited")
168
+ return
169
+
170
+ # Try to create commit with tool name as prefix
171
+ success = commit_for_undo(message_prefix=f"tunacode {self.tool_name.lower()}")
172
+ if success and self.ui:
173
+ await self.ui.muted("• Git commit created for undo tracking")
174
+ elif self.ui:
175
+ await self.ui.muted("⚠️ Could not create git commit - undo may not work")
176
+ except Exception:
177
+ # Silently ignore commit errors - don't break the main file operation
178
+ # The file operation itself succeeded, we just can't track it for undo
179
+ if self.ui:
180
+ try:
181
+ await self.ui.muted("⚠️ Git commit failed - undo functionality limited")
182
+ except:
183
+ # Even the warning failed, just continue silently
184
+ pass
185
+
186
+ def _format_args(self, filepath: FilePath, *args, **kwargs) -> str:
187
+ """Format arguments with filepath as first argument."""
188
+ # Always show the filepath first
189
+ all_args = [repr(filepath)]
190
+
191
+ # Add remaining positional arguments
192
+ for arg in args:
193
+ if isinstance(arg, str) and len(arg) > 50:
194
+ all_args.append(f"'{arg[:47]}...'")
195
+ else:
196
+ all_args.append(repr(arg))
197
+
198
+ # Add keyword arguments
199
+ for key, value in kwargs.items():
200
+ if isinstance(value, str) and len(value) > 50:
201
+ all_args.append(f"{key}='{value[:47]}...'")
202
+ else:
203
+ all_args.append(f"{key}={repr(value)}")
204
+
205
+ return ", ".join(all_args)
206
+
207
+ def _get_error_context(self, filepath: FilePath = None, *args, **kwargs) -> str:
208
+ """Get error context including file path."""
209
+ if filepath:
210
+ return f"handling file '{filepath}'"
211
+ return super()._get_error_context(*args, **kwargs)
212
+
213
+ async def _handle_error(self, error: Exception, *args, **kwargs) -> ToolResult:
214
+ """Handle file-specific errors.
215
+
216
+ Overrides base class to create FileOperationError for file-related issues.
217
+
218
+ Raises:
219
+ ToolExecutionError: Always raised with structured error information
220
+ """
221
+ filepath = args[0] if args else kwargs.get("filepath", "unknown")
222
+
223
+ # Check if this is a file-related error
224
+ if isinstance(error, (IOError, OSError, PermissionError, FileNotFoundError)):
225
+ # Determine the operation based on the tool name
226
+ operation = self.tool_name.replace("_", " ")
227
+
228
+ # Create a FileOperationError
229
+ file_error = FileOperationError(
230
+ operation=operation, path=str(filepath), message=str(error), original_error=error
231
+ )
232
+
233
+ # Format error message for display
234
+ err_msg = str(file_error)
235
+ if self.ui:
236
+ await self.ui.error(err_msg)
237
+
238
+ # Raise ToolExecutionError with the file error
239
+ raise ToolExecutionError(
240
+ tool_name=self.tool_name, message=str(file_error), original_error=file_error
241
+ )
242
+
243
+ # For non-file errors, use the base class handling
244
+ await super()._handle_error(error, *args, **kwargs)
@@ -0,0 +1,89 @@
1
+ """
2
+ Module: sidekick.tools.read_file
3
+
4
+ File reading tool for agent operations in the Sidekick application.
5
+ Provides safe file reading with size limits and proper error handling.
6
+ """
7
+
8
+ import os
9
+
10
+ from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
11
+ ERROR_FILE_TOO_LARGE, MAX_FILE_SIZE, MSG_FILE_SIZE_LIMIT)
12
+ from tunacode.exceptions import ToolExecutionError
13
+ from tunacode.tools.base import FileBasedTool
14
+ from tunacode.types import FilePath, ToolResult
15
+ from tunacode.ui import console as default_ui
16
+
17
+
18
+ class ReadFileTool(FileBasedTool):
19
+ """Tool for reading file contents."""
20
+
21
+ @property
22
+ def tool_name(self) -> str:
23
+ return "Read"
24
+
25
+ async def _execute(self, filepath: FilePath) -> ToolResult:
26
+ """Read the contents of a file.
27
+
28
+ Args:
29
+ filepath: The path to the file to read.
30
+
31
+ Returns:
32
+ ToolResult: The contents of the file or an error message.
33
+
34
+ Raises:
35
+ Exception: Any file reading errors
36
+ """
37
+ # Add a size limit to prevent reading huge files
38
+ if os.path.getsize(filepath) > MAX_FILE_SIZE:
39
+ err_msg = ERROR_FILE_TOO_LARGE.format(filepath=filepath) + MSG_FILE_SIZE_LIMIT
40
+ if self.ui:
41
+ await self.ui.error(err_msg)
42
+ raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=None)
43
+
44
+ with open(filepath, "r", encoding="utf-8") as file:
45
+ content = file.read()
46
+ return content
47
+
48
+ async def _handle_error(self, error: Exception, filepath: FilePath = None) -> ToolResult:
49
+ """Handle errors with specific messages for common cases.
50
+
51
+ Raises:
52
+ ToolExecutionError: Always raised with structured error information
53
+ """
54
+ if isinstance(error, FileNotFoundError):
55
+ err_msg = ERROR_FILE_NOT_FOUND.format(filepath=filepath)
56
+ elif isinstance(error, UnicodeDecodeError):
57
+ err_msg = (
58
+ ERROR_FILE_DECODE.format(filepath=filepath)
59
+ + " "
60
+ + ERROR_FILE_DECODE_DETAILS.format(error=error)
61
+ )
62
+ else:
63
+ # Use parent class handling for other errors
64
+ await super()._handle_error(error, filepath)
65
+ return # super() will raise, this is unreachable
66
+
67
+ if self.ui:
68
+ await self.ui.error(err_msg)
69
+
70
+ raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=error)
71
+
72
+
73
+ # Create the function that maintains the existing interface
74
+ async def read_file(filepath: FilePath) -> ToolResult:
75
+ """
76
+ Read the contents of a file.
77
+
78
+ Args:
79
+ filepath (FilePath): The path to the file to read.
80
+
81
+ Returns:
82
+ ToolResult: The contents of the file or an error message.
83
+ """
84
+ tool = ReadFileTool(default_ui)
85
+ try:
86
+ return await tool.execute(filepath)
87
+ except ToolExecutionError as e:
88
+ # Return error message for pydantic-ai compatibility
89
+ return str(e)