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.
- tunacode/cli/commands/__init__.py +2 -2
- tunacode/cli/commands/implementations/__init__.py +2 -3
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/quickstart.py +43 -0
- tunacode/cli/commands/implementations/system.py +96 -3
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +139 -5
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +135 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +400 -0
- tunacode/cli/main.py +23 -2
- tunacode/cli/repl.py +217 -190
- tunacode/cli/repl_components/command_parser.py +38 -4
- tunacode/cli/repl_components/error_recovery.py +85 -4
- tunacode/cli/repl_components/output_display.py +12 -1
- tunacode/cli/repl_components/tool_executor.py +1 -1
- tunacode/configuration/defaults.py +12 -3
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +12 -40
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +249 -55
- tunacode/core/agents/agent_components/agent_helpers.py +43 -13
- tunacode/core/agents/agent_components/node_processor.py +179 -139
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -121
- tunacode/core/code_index.py +83 -29
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +110 -20
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +14 -5
- tunacode/core/state.py +16 -20
- tunacode/core/token_usage/usage_tracker.py +5 -3
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -40
- tunacode/exceptions.py +119 -5
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +125 -7
- tunacode/setup.py +5 -25
- tunacode/tools/base.py +163 -0
- tunacode/tools/bash.py +110 -1
- tunacode/tools/glob.py +332 -34
- tunacode/tools/grep.py +179 -82
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/prompts/bash_prompt.xml +72 -0
- tunacode/tools/prompts/glob_prompt.xml +45 -0
- tunacode/tools/prompts/grep_prompt.xml +98 -0
- tunacode/tools/prompts/list_dir_prompt.xml +31 -0
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +54 -0
- tunacode/tools/prompts/run_command_prompt.xml +64 -0
- tunacode/tools/prompts/update_file_prompt.xml +53 -0
- tunacode/tools/prompts/write_file_prompt.xml +37 -0
- tunacode/tools/react.py +153 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +114 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/tools/xml_helper.py +83 -0
- tunacode/tutorial/__init__.py +9 -0
- tunacode/tutorial/content.py +98 -0
- tunacode/tutorial/manager.py +182 -0
- tunacode/tutorial/steps.py +124 -0
- tunacode/types.py +20 -27
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +20 -3
- tunacode/ui/keybindings.py +7 -4
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +212 -43
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +5 -1
- tunacode/ui/tool_ui.py +33 -10
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/ripgrep.py +332 -9
- tunacode/utils/text_utils.py +18 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
- tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -182
- tunacode/prompts/system.md +0 -731
- tunacode/tools/read_file_async_poc.py +0 -196
- tunacode/tools/todo.py +0 -349
- tunacode_cli-0.0.55.dist-info/METADATA +0 -322
- tunacode_cli-0.0.55.dist-info/RECORD +0 -126
- tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
tunacode/tools/read_file.py
CHANGED
|
@@ -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
|
|
tunacode/tools/run_command.py
CHANGED
|
@@ -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)
|
tunacode/tools/update_file.py
CHANGED
|
@@ -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
|
|
tunacode/tools/write_file.py
CHANGED
|
@@ -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
|
|