tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__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 (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,13 @@ Provides safe file reading with size limits and proper error handling.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import logging
9
10
  import os
11
+ from functools import lru_cache
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List
14
+
15
+ import defusedxml.ElementTree as ET
10
16
 
11
17
  from tunacode.constants import (
12
18
  ERROR_FILE_DECODE,
@@ -20,6 +26,8 @@ from tunacode.exceptions import ToolExecutionError
20
26
  from tunacode.tools.base import FileBasedTool
21
27
  from tunacode.types import ToolResult
22
28
 
29
+ logger = logging.getLogger(__name__)
30
+
23
31
 
24
32
  class ReadFileTool(FileBasedTool):
25
33
  """Tool for reading file contents."""
@@ -28,6 +36,89 @@ class ReadFileTool(FileBasedTool):
28
36
  def tool_name(self) -> str:
29
37
  return "Read"
30
38
 
39
+ @lru_cache(maxsize=1)
40
+ def _get_base_prompt(self) -> str:
41
+ """Load and return the base prompt from XML file.
42
+
43
+ Returns:
44
+ str: The loaded prompt from XML or a default prompt
45
+ """
46
+ try:
47
+ # Load prompt from XML file
48
+ prompt_file = Path(__file__).parent / "prompts" / "read_file_prompt.xml"
49
+ if prompt_file.exists():
50
+ tree = ET.parse(prompt_file)
51
+ root = tree.getroot()
52
+ description = root.find("description")
53
+ if description is not None:
54
+ return description.text.strip()
55
+ except Exception as e:
56
+ logger.warning(f"Failed to load XML prompt for read_file: {e}")
57
+
58
+ # Fallback to default prompt
59
+ return """Reads a file from the local filesystem"""
60
+
61
+ @lru_cache(maxsize=1)
62
+ def _get_parameters_schema(self) -> Dict[str, Any]:
63
+ """Get the parameters schema for read_file tool.
64
+
65
+ Returns:
66
+ Dict containing the JSON schema for tool parameters
67
+ """
68
+ # Try to load from XML first
69
+ try:
70
+ prompt_file = Path(__file__).parent / "prompts" / "read_file_prompt.xml"
71
+ if prompt_file.exists():
72
+ tree = ET.parse(prompt_file)
73
+ root = tree.getroot()
74
+ parameters = root.find("parameters")
75
+ if parameters is not None:
76
+ schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
77
+ required_fields: List[str] = []
78
+
79
+ for param in parameters.findall("parameter"):
80
+ name = param.get("name")
81
+ required = param.get("required", "false").lower() == "true"
82
+ param_type = param.find("type")
83
+ description = param.find("description")
84
+
85
+ if name and param_type is not None:
86
+ prop = {
87
+ "type": param_type.text.strip(),
88
+ "description": description.text.strip()
89
+ if description is not None
90
+ else "",
91
+ }
92
+
93
+ schema["properties"][name] = prop
94
+ if required:
95
+ required_fields.append(name)
96
+
97
+ schema["required"] = required_fields
98
+ return schema
99
+ except Exception as e:
100
+ logger.warning(f"Failed to load parameters from XML for read_file: {e}")
101
+
102
+ # Fallback to hardcoded schema
103
+ return {
104
+ "type": "object",
105
+ "properties": {
106
+ "file_path": {
107
+ "type": "string",
108
+ "description": "The absolute path to the file to read",
109
+ },
110
+ "offset": {
111
+ "type": "number",
112
+ "description": "The line number to start reading from",
113
+ },
114
+ "limit": {
115
+ "type": "number",
116
+ "description": "The number of lines to read",
117
+ },
118
+ },
119
+ "required": ["file_path"],
120
+ }
121
+
31
122
  async def _execute(self, filepath: str) -> ToolResult:
32
123
  """Read the contents of a file.
33
124
 
@@ -5,7 +5,13 @@ Command execution tool for agent operations in the TunaCode application.
5
5
  Provides controlled shell command execution with output capture and truncation.
6
6
  """
7
7
 
8
+ import logging
8
9
  import subprocess
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List
13
+
14
+ import defusedxml.ElementTree as ET
9
15
 
10
16
  from tunacode.constants import (
11
17
  CMD_OUTPUT_FORMAT,
@@ -23,10 +29,103 @@ from tunacode.tools.base import BaseTool
23
29
  from tunacode.types import ToolResult
24
30
  from tunacode.utils.security import CommandSecurityError, safe_subprocess_popen
25
31
 
32
+ logger = logging.getLogger(__name__)
33
+
26
34
 
27
35
  class RunCommandTool(BaseTool):
28
36
  """Tool for running shell commands."""
29
37
 
38
+ @lru_cache(maxsize=1)
39
+ def _get_base_prompt(self) -> str:
40
+ """Load and return the base prompt from XML file.
41
+
42
+ Returns:
43
+ str: The loaded prompt from XML or a default prompt
44
+ """
45
+ try:
46
+ # Load prompt from XML file
47
+ prompt_file = Path(__file__).parent / "prompts" / "run_command_prompt.xml"
48
+ if prompt_file.exists():
49
+ tree = ET.parse(prompt_file)
50
+ root = tree.getroot()
51
+ description = root.find("description")
52
+ if description is not None:
53
+ return description.text.strip()
54
+ except Exception as e:
55
+ logger.warning(f"Failed to load XML prompt for run_command: {e}")
56
+
57
+ # Fallback to default prompt
58
+ return """Executes system commands with enhanced control and monitoring capabilities"""
59
+
60
+ @lru_cache(maxsize=1)
61
+ def _get_parameters_schema(self) -> Dict[str, Any]:
62
+ """Get the parameters schema for run_command tool.
63
+
64
+ Returns:
65
+ Dict containing the JSON schema for tool parameters
66
+ """
67
+ # Try to load from XML first
68
+ try:
69
+ prompt_file = Path(__file__).parent / "prompts" / "run_command_prompt.xml"
70
+ if prompt_file.exists():
71
+ tree = ET.parse(prompt_file)
72
+ root = tree.getroot()
73
+ parameters = root.find("parameters")
74
+ if parameters is not None:
75
+ schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
76
+ required_fields: List[str] = []
77
+
78
+ for param in parameters.findall("parameter"):
79
+ name = param.get("name")
80
+ required = param.get("required", "false").lower() == "true"
81
+ param_type = param.find("type")
82
+ description = param.find("description")
83
+
84
+ if name and param_type is not None:
85
+ prop = {
86
+ "type": param_type.text.strip(),
87
+ "description": description.text.strip()
88
+ if description is not None
89
+ else "",
90
+ }
91
+
92
+ schema["properties"][name] = prop
93
+ if required:
94
+ required_fields.append(name)
95
+
96
+ schema["required"] = required_fields
97
+ return schema
98
+ except Exception as e:
99
+ logger.warning(f"Failed to load parameters from XML for run_command: {e}")
100
+
101
+ # Fallback to hardcoded schema
102
+ return {
103
+ "type": "object",
104
+ "properties": {
105
+ "command": {
106
+ "type": "string",
107
+ "description": "The command to execute",
108
+ },
109
+ "cwd": {
110
+ "type": "string",
111
+ "description": "Working directory for the command",
112
+ },
113
+ "env": {
114
+ "type": "object",
115
+ "description": "Additional environment variables",
116
+ },
117
+ "timeout": {
118
+ "type": "integer",
119
+ "description": "Command timeout in seconds",
120
+ },
121
+ "capture_output": {
122
+ "type": "boolean",
123
+ "description": "Whether to capture stdout/stderr",
124
+ },
125
+ },
126
+ "required": ["command"],
127
+ }
128
+
30
129
  @property
31
130
  def tool_name(self) -> str:
32
131
  return "Shell"
@@ -45,6 +144,7 @@ class RunCommandTool(BaseTool):
45
144
  CommandSecurityError: If command fails security validation
46
145
  Exception: Any command execution errors
47
146
  """
147
+ process = None
48
148
  try:
49
149
  # Use secure subprocess execution with validation
50
150
  process = safe_subprocess_popen(
@@ -59,6 +159,20 @@ class RunCommandTool(BaseTool):
59
159
  except CommandSecurityError as e:
60
160
  # Security validation failed - return error without execution
61
161
  return f"Security validation failed: {str(e)}"
162
+ finally:
163
+ # Ensure process cleanup regardless of success or failure
164
+ if process is not None and process.poll() is None:
165
+ try:
166
+ # Multi-stage escalation: graceful → terminate → kill
167
+ process.terminate()
168
+ try:
169
+ process.wait(timeout=5.0)
170
+ except subprocess.TimeoutExpired:
171
+ process.kill()
172
+ process.wait(timeout=1.0)
173
+ except Exception as cleanup_error:
174
+ self.logger.warning(f"Failed to cleanup process: {cleanup_error}")
175
+
62
176
  output = stdout.strip() or CMD_OUTPUT_NO_OUTPUT
63
177
  error = stderr.strip() or CMD_OUTPUT_NO_ERRORS
64
178
  resp = CMD_OUTPUT_FORMAT.format(output=output, error=error).strip()
@@ -0,0 +1,167 @@
1
+ """Tool Schema Assembler for API Integration.
2
+
3
+ This module handles the assembly of tool schemas for API calls,
4
+ converting tool prompts and parameters into OpenAI-compatible function schemas.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Type
8
+
9
+ from tunacode.tools.base import BaseTool
10
+
11
+
12
+ class ToolSchemaAssembler:
13
+ """Assembles tool schemas for API integration."""
14
+
15
+ def __init__(self):
16
+ """Initialize the schema assembler."""
17
+ self._context: Dict[str, Any] = {}
18
+ self._tool_instances: Dict[str, BaseTool] = {}
19
+
20
+ def set_context(self, context: Dict[str, Any]) -> None:
21
+ """Set the context for all tools.
22
+
23
+ Args:
24
+ context: Context including model, permissions, environment, etc.
25
+ """
26
+ self._context = context
27
+ # Update context for all registered tools
28
+ for tool in self._tool_instances.values():
29
+ tool._context.update(context)
30
+
31
+ def register_tool(self, tool: BaseTool) -> None:
32
+ """Register a tool instance.
33
+
34
+ Args:
35
+ tool: The tool instance to register
36
+ """
37
+ self._tool_instances[tool.tool_name] = tool
38
+ # Apply current context to the new tool
39
+ if self._context:
40
+ tool._context.update(self._context)
41
+
42
+ def register_tool_class(self, tool_class: Type[BaseTool], *args, **kwargs) -> None:
43
+ """Register a tool by instantiating its class.
44
+
45
+ Args:
46
+ tool_class: The tool class to instantiate
47
+ *args, **kwargs: Arguments for tool instantiation
48
+ """
49
+ tool = tool_class(*args, **kwargs)
50
+ self.register_tool(tool)
51
+
52
+ def get_tool_schema(self, tool_name: str) -> Optional[Dict[str, Any]]:
53
+ """Get the schema for a specific tool.
54
+
55
+ Args:
56
+ tool_name: Name of the tool
57
+
58
+ Returns:
59
+ Tool schema in OpenAI function format, or None if not found
60
+ """
61
+ tool = self._tool_instances.get(tool_name)
62
+ if not tool:
63
+ return None
64
+
65
+ return tool.get_tool_schema()
66
+
67
+ def get_all_schemas(self) -> List[Dict[str, Any]]:
68
+ """Get schemas for all registered tools.
69
+
70
+ Returns:
71
+ List of tool schemas in OpenAI function format
72
+ """
73
+ schemas = []
74
+ for tool in self._tool_instances.values():
75
+ schema = tool.get_tool_schema()
76
+ if schema:
77
+ schemas.append(schema)
78
+ return schemas
79
+
80
+ def get_schemas_for_model(self, model: str) -> List[Dict[str, Any]]:
81
+ """Get schemas optimized for a specific model.
82
+
83
+ Args:
84
+ model: The model identifier (e.g., 'claude-3', 'gpt-4')
85
+
86
+ Returns:
87
+ List of tool schemas optimized for the model
88
+ """
89
+ # Update context with model
90
+ self.set_context({"model": model, **self._context})
91
+
92
+ # Get all schemas with model-specific prompts
93
+ return self.get_all_schemas()
94
+
95
+ def get_schemas_with_permissions(self, permissions: Dict[str, Any]) -> List[Dict[str, Any]]:
96
+ """Get schemas filtered by permissions.
97
+
98
+ Args:
99
+ permissions: Permission settings
100
+
101
+ Returns:
102
+ List of tool schemas filtered by permissions
103
+ """
104
+ # Update context with permissions
105
+ self.set_context({"permissions": permissions, **self._context})
106
+
107
+ # Filter tools based on permissions
108
+ schemas = []
109
+ for tool_name, tool in self._tool_instances.items():
110
+ # Check if tool is allowed based on permissions
111
+ if self._is_tool_allowed(tool_name, permissions):
112
+ schema = tool.get_tool_schema()
113
+ if schema:
114
+ schemas.append(schema)
115
+
116
+ return schemas
117
+
118
+ def _is_tool_allowed(self, tool_name: str, permissions: Dict[str, Any]) -> bool:
119
+ """Check if a tool is allowed based on permissions.
120
+
121
+ Args:
122
+ tool_name: Name of the tool
123
+ permissions: Permission settings
124
+
125
+ Returns:
126
+ True if the tool is allowed
127
+ """
128
+ # Default implementation - can be extended
129
+ if permissions.get("restricted", False):
130
+ # In restricted mode, only allow safe tools
131
+ safe_tools = ["read_file", "list_dir", "grep", "glob"]
132
+ return tool_name in safe_tools
133
+
134
+ # Check for explicit tool permissions
135
+ allowed_tools = permissions.get("allowed_tools")
136
+ if allowed_tools:
137
+ return tool_name in allowed_tools
138
+
139
+ blocked_tools = permissions.get("blocked_tools", [])
140
+ return tool_name not in blocked_tools
141
+
142
+ def refresh_prompts(self) -> None:
143
+ """Refresh prompts for all tools based on current context."""
144
+ for tool in self._tool_instances.values():
145
+ # Clear prompt cache to force regeneration
146
+ tool._prompt_cache = None
147
+
148
+ def update_environment(self, env_vars: Dict[str, Any]) -> None:
149
+ """Update environment variables in context.
150
+
151
+ Args:
152
+ env_vars: Environment variables to update
153
+ """
154
+ current_env = self._context.get("environment", {})
155
+ current_env.update(env_vars)
156
+ self.set_context({"environment": current_env, **self._context})
157
+
158
+ def get_tool_by_name(self, tool_name: str) -> Optional[BaseTool]:
159
+ """Get a tool instance by name.
160
+
161
+ Args:
162
+ tool_name: Name of the tool
163
+
164
+ Returns:
165
+ Tool instance or None if not found
166
+ """
167
+ return self._tool_instances.get(tool_name)
@@ -5,14 +5,21 @@ File update tool for agent operations in the TunaCode application.
5
5
  Provides targeted file content modification with diff-based updates.
6
6
  """
7
7
 
8
+ import logging
8
9
  import os
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List
9
13
 
14
+ import defusedxml.ElementTree as ET
10
15
  from pydantic_ai.exceptions import ModelRetry
11
16
 
12
17
  from tunacode.exceptions import ToolExecutionError
13
18
  from tunacode.tools.base import FileBasedTool
14
19
  from tunacode.types import ToolResult
15
20
 
21
+ logger = logging.getLogger(__name__)
22
+
16
23
 
17
24
  class UpdateFileTool(FileBasedTool):
18
25
  """Tool for updating existing files by replacing text blocks."""
@@ -21,6 +28,93 @@ class UpdateFileTool(FileBasedTool):
21
28
  def tool_name(self) -> str:
22
29
  return "Update"
23
30
 
31
+ @lru_cache(maxsize=1)
32
+ def _get_base_prompt(self) -> str:
33
+ """Load and return the base prompt from XML file.
34
+
35
+ Returns:
36
+ str: The loaded prompt from XML or a default prompt
37
+ """
38
+ try:
39
+ # Load prompt from XML file
40
+ prompt_file = Path(__file__).parent / "prompts" / "update_file_prompt.xml"
41
+ if prompt_file.exists():
42
+ tree = ET.parse(prompt_file)
43
+ root = tree.getroot()
44
+ description = root.find("description")
45
+ if description is not None:
46
+ return description.text.strip()
47
+ except Exception as e:
48
+ logger.warning(f"Failed to load XML prompt for update_file: {e}")
49
+
50
+ # Fallback to default prompt
51
+ return """Performs exact string replacements in files"""
52
+
53
+ @lru_cache(maxsize=1)
54
+ def _get_parameters_schema(self) -> Dict[str, Any]:
55
+ """Get the parameters schema for update_file tool.
56
+
57
+ Returns:
58
+ Dict containing the JSON schema for tool parameters
59
+ """
60
+ # Try to load from XML first
61
+ try:
62
+ prompt_file = Path(__file__).parent / "prompts" / "update_file_prompt.xml"
63
+ if prompt_file.exists():
64
+ tree = ET.parse(prompt_file)
65
+ root = tree.getroot()
66
+ parameters = root.find("parameters")
67
+ if parameters is not None:
68
+ schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
69
+ required_fields: List[str] = []
70
+
71
+ for param in parameters.findall("parameter"):
72
+ name = param.get("name")
73
+ required = param.get("required", "false").lower() == "true"
74
+ param_type = param.find("type")
75
+ description = param.find("description")
76
+
77
+ if name and param_type is not None:
78
+ prop = {
79
+ "type": param_type.text.strip(),
80
+ "description": description.text.strip()
81
+ if description is not None
82
+ else "",
83
+ }
84
+
85
+ schema["properties"][name] = prop
86
+ if required:
87
+ required_fields.append(name)
88
+
89
+ schema["required"] = required_fields
90
+ return schema
91
+ except Exception as e:
92
+ logger.warning(f"Failed to load parameters from XML for update_file: {e}")
93
+
94
+ # Fallback to hardcoded schema
95
+ return {
96
+ "type": "object",
97
+ "properties": {
98
+ "file_path": {
99
+ "type": "string",
100
+ "description": "The absolute path to the file to modify",
101
+ },
102
+ "old_string": {
103
+ "type": "string",
104
+ "description": "The text to replace",
105
+ },
106
+ "new_string": {
107
+ "type": "string",
108
+ "description": "The text to replace it with",
109
+ },
110
+ "replace_all": {
111
+ "type": "boolean",
112
+ "description": "Replace all occurences of old_string",
113
+ },
114
+ },
115
+ "required": ["file_path", "old_string", "new_string"],
116
+ }
117
+
24
118
  async def _execute(self, filepath: str, target: str, patch: str) -> ToolResult:
25
119
  """Update an existing file by replacing a target text block with a patch.
26
120
 
@@ -5,14 +5,21 @@ File writing tool for agent operations in the TunaCode application.
5
5
  Provides safe file creation with conflict detection and encoding handling.
6
6
  """
7
7
 
8
+ import logging
8
9
  import os
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List
9
13
 
14
+ import defusedxml.ElementTree as ET
10
15
  from pydantic_ai.exceptions import ModelRetry
11
16
 
12
17
  from tunacode.exceptions import ToolExecutionError
13
18
  from tunacode.tools.base import FileBasedTool
14
19
  from tunacode.types import ToolResult
15
20
 
21
+ logger = logging.getLogger(__name__)
22
+
16
23
 
17
24
  class WriteFileTool(FileBasedTool):
18
25
  """Tool for writing content to new files."""
@@ -21,6 +28,85 @@ class WriteFileTool(FileBasedTool):
21
28
  def tool_name(self) -> str:
22
29
  return "Write"
23
30
 
31
+ @lru_cache(maxsize=1)
32
+ def _get_base_prompt(self) -> str:
33
+ """Load and return the base prompt from XML file.
34
+
35
+ Returns:
36
+ str: The loaded prompt from XML or a default prompt
37
+ """
38
+ try:
39
+ # Load prompt from XML file
40
+ prompt_file = Path(__file__).parent / "prompts" / "write_file_prompt.xml"
41
+ if prompt_file.exists():
42
+ tree = ET.parse(prompt_file)
43
+ root = tree.getroot()
44
+ description = root.find("description")
45
+ if description is not None:
46
+ return description.text.strip()
47
+ except Exception as e:
48
+ logger.warning(f"Failed to load XML prompt for write_file: {e}")
49
+
50
+ # Fallback to default prompt
51
+ return """Writes a file to the local filesystem"""
52
+
53
+ @lru_cache(maxsize=1)
54
+ def _get_parameters_schema(self) -> Dict[str, Any]:
55
+ """Get the parameters schema for write_file tool.
56
+
57
+ Returns:
58
+ Dict containing the JSON schema for tool parameters
59
+ """
60
+ # Try to load from XML first
61
+ try:
62
+ prompt_file = Path(__file__).parent / "prompts" / "write_file_prompt.xml"
63
+ if prompt_file.exists():
64
+ tree = ET.parse(prompt_file)
65
+ root = tree.getroot()
66
+ parameters = root.find("parameters")
67
+ if parameters is not None:
68
+ schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
69
+ required_fields: List[str] = []
70
+
71
+ for param in parameters.findall("parameter"):
72
+ name = param.get("name")
73
+ required = param.get("required", "false").lower() == "true"
74
+ param_type = param.find("type")
75
+ description = param.find("description")
76
+
77
+ if name and param_type is not None:
78
+ prop = {
79
+ "type": param_type.text.strip(),
80
+ "description": description.text.strip()
81
+ if description is not None
82
+ else "",
83
+ }
84
+
85
+ schema["properties"][name] = prop
86
+ if required:
87
+ required_fields.append(name)
88
+
89
+ schema["required"] = required_fields
90
+ return schema
91
+ except Exception as e:
92
+ logger.warning(f"Failed to load parameters from XML for write_file: {e}")
93
+
94
+ # Fallback to hardcoded schema
95
+ return {
96
+ "type": "object",
97
+ "properties": {
98
+ "file_path": {
99
+ "type": "string",
100
+ "description": "The absolute path to the file to write",
101
+ },
102
+ "content": {
103
+ "type": "string",
104
+ "description": "The content to write to the file",
105
+ },
106
+ },
107
+ "required": ["file_path", "content"],
108
+ }
109
+
24
110
  async def _execute(self, filepath: str, content: str) -> ToolResult:
25
111
  """Write content to a new file. Fails if the file already exists.
26
112