tunacode-cli 0.0.56__py3-none-any.whl → 0.0.60__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 (45) hide show
  1. tunacode/cli/commands/implementations/plan.py +8 -8
  2. tunacode/cli/commands/registry.py +2 -2
  3. tunacode/cli/repl.py +214 -407
  4. tunacode/cli/repl_components/command_parser.py +37 -4
  5. tunacode/cli/repl_components/error_recovery.py +79 -1
  6. tunacode/cli/repl_components/output_display.py +14 -11
  7. tunacode/cli/repl_components/tool_executor.py +7 -4
  8. tunacode/configuration/defaults.py +8 -0
  9. tunacode/constants.py +8 -2
  10. tunacode/core/agents/agent_components/agent_config.py +128 -65
  11. tunacode/core/agents/agent_components/node_processor.py +6 -2
  12. tunacode/core/code_index.py +83 -29
  13. tunacode/core/state.py +1 -1
  14. tunacode/core/token_usage/usage_tracker.py +2 -2
  15. tunacode/core/tool_handler.py +3 -3
  16. tunacode/prompts/system.md +117 -490
  17. tunacode/services/mcp.py +29 -7
  18. tunacode/tools/base.py +110 -0
  19. tunacode/tools/bash.py +96 -1
  20. tunacode/tools/exit_plan_mode.py +114 -32
  21. tunacode/tools/glob.py +366 -33
  22. tunacode/tools/grep.py +226 -77
  23. tunacode/tools/grep_components/result_formatter.py +98 -4
  24. tunacode/tools/list_dir.py +132 -2
  25. tunacode/tools/present_plan.py +111 -31
  26. tunacode/tools/read_file.py +91 -0
  27. tunacode/tools/run_command.py +99 -0
  28. tunacode/tools/schema_assembler.py +167 -0
  29. tunacode/tools/todo.py +108 -1
  30. tunacode/tools/update_file.py +94 -0
  31. tunacode/tools/write_file.py +86 -0
  32. tunacode/types.py +10 -9
  33. tunacode/ui/input.py +1 -0
  34. tunacode/ui/keybindings.py +1 -0
  35. tunacode/ui/panels.py +49 -27
  36. tunacode/ui/prompt_manager.py +13 -7
  37. tunacode/utils/json_utils.py +206 -0
  38. tunacode/utils/ripgrep.py +332 -9
  39. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/METADATA +7 -2
  40. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/RECORD +44 -43
  41. tunacode/tools/read_file_async_poc.py +0 -196
  42. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/WHEEL +0 -0
  43. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/entry_points.txt +0 -0
  44. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/licenses/LICENSE +0 -0
  45. {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.60.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,108 @@
1
1
  """Tool for presenting a structured plan and exiting plan mode."""
2
2
 
3
- from typing import List, Optional
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import defusedxml.ElementTree as ET
4
8
 
5
9
  from tunacode.tools.base import BaseTool
10
+ from tunacode.types import PlanDoc, PlanPhase, ToolResult
6
11
  from tunacode.ui import console as ui
7
- from tunacode.types import ToolResult, PlanDoc, PlanPhase
12
+
13
+ logger = logging.getLogger(__name__)
8
14
 
9
15
 
10
16
  class PresentPlanTool(BaseTool):
11
17
  """Present a structured implementation plan and request user approval."""
12
-
18
+
13
19
  def __init__(self, state_manager, ui_logger=None):
14
20
  """Initialize the present plan tool.
15
-
21
+
16
22
  Args:
17
23
  state_manager: StateManager instance for controlling plan mode state
18
24
  ui_logger: UI logger instance for displaying messages
19
25
  """
20
26
  super().__init__(ui_logger)
21
27
  self.state_manager = state_manager
22
-
28
+
23
29
  @property
24
30
  def tool_name(self) -> str:
25
31
  return "present_plan"
26
-
32
+
33
+ def _get_base_prompt(self) -> str:
34
+ """Load and return the base prompt from XML file.
35
+
36
+ Returns:
37
+ str: The loaded prompt from XML or a default prompt
38
+ """
39
+ try:
40
+ # Load prompt from XML file
41
+ prompt_file = Path(__file__).parent / "prompts" / "present_plan_prompt.xml"
42
+ if prompt_file.exists():
43
+ tree = ET.parse(prompt_file)
44
+ root = tree.getroot()
45
+ description = root.find("description")
46
+ if description is not None:
47
+ return description.text.strip()
48
+ except Exception as e:
49
+ logger.warning(f"Failed to load XML prompt for present_plan: {e}")
50
+
51
+ # Fallback to default prompt
52
+ return """Present a plan to the user for approval before execution"""
53
+
54
+ def _get_parameters_schema(self) -> Dict[str, Any]:
55
+ """Get the parameters schema for present_plan 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" / "present_plan_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 present_plan: {e}")
93
+
94
+ # Fallback to hardcoded schema
95
+ return {
96
+ "type": "object",
97
+ "properties": {
98
+ "plan": {
99
+ "type": "string",
100
+ "description": "The plan to present to the user",
101
+ },
102
+ },
103
+ "required": ["plan"],
104
+ }
105
+
27
106
  async def _execute(
28
107
  self,
29
108
  title: str,
@@ -39,7 +118,7 @@ class PresentPlanTool(BaseTool):
39
118
  references: List[str] = None,
40
119
  ) -> ToolResult:
41
120
  """Present the implementation plan for user approval."""
42
-
121
+
43
122
  # Create PlanDoc from parameters
44
123
  plan_doc = PlanDoc(
45
124
  title=title,
@@ -52,21 +131,21 @@ class PresentPlanTool(BaseTool):
52
131
  rollback=rollback,
53
132
  open_questions=open_questions or [],
54
133
  success_criteria=success_criteria or [],
55
- references=references or []
134
+ references=references or [],
56
135
  )
57
-
136
+
58
137
  # Validate the plan
59
138
  is_valid, missing_sections = plan_doc.validate()
60
139
  if not is_valid:
61
140
  return f"❌ Plan incomplete. Missing sections: {', '.join(missing_sections)}. Continue researching and refining your plan."
62
-
141
+
63
142
  # Set plan phase to PLAN_READY and store the plan
64
143
  # The REPL will handle displaying the plan when it detects PLAN_READY phase
65
144
  self.state_manager.session.plan_phase = PlanPhase.PLAN_READY
66
145
  self.state_manager.session.current_plan = plan_doc
67
-
146
+
68
147
  return "Plan ready for review. The system will now present it to the user for approval."
69
-
148
+
70
149
  async def _present_plan(self, plan_doc: PlanDoc) -> None:
71
150
  """Present the plan in a formatted way."""
72
151
  output = []
@@ -77,70 +156,70 @@ class PresentPlanTool(BaseTool):
77
156
  output.append("")
78
157
  output.append(f"🎯 **{plan_doc.title}**")
79
158
  output.append("")
80
-
159
+
81
160
  if plan_doc.overview:
82
161
  output.append(f"📝 **Overview:** {plan_doc.overview}")
83
162
  output.append("")
84
-
163
+
85
164
  # Files section
86
165
  if plan_doc.files_to_modify:
87
166
  output.append("📝 **Files to Modify:**")
88
167
  for f in plan_doc.files_to_modify:
89
168
  output.append(f" • {f}")
90
169
  output.append("")
91
-
170
+
92
171
  if plan_doc.files_to_create:
93
172
  output.append("📄 **Files to Create:**")
94
173
  for f in plan_doc.files_to_create:
95
174
  output.append(f" • {f}")
96
175
  output.append("")
97
-
176
+
98
177
  # Implementation steps
99
178
  output.append("🔧 **Implementation Steps:**")
100
179
  for i, step in enumerate(plan_doc.steps, 1):
101
180
  output.append(f" {i}. {step}")
102
181
  output.append("")
103
-
182
+
104
183
  # Testing approach
105
184
  if plan_doc.tests:
106
185
  output.append("🧪 **Testing Approach:**")
107
186
  for test in plan_doc.tests:
108
187
  output.append(f" • {test}")
109
188
  output.append("")
110
-
189
+
111
190
  # Success criteria
112
191
  if plan_doc.success_criteria:
113
192
  output.append("✅ **Success Criteria:**")
114
193
  for criteria in plan_doc.success_criteria:
115
194
  output.append(f" • {criteria}")
116
195
  output.append("")
117
-
196
+
118
197
  # Risks and considerations
119
198
  if plan_doc.risks:
120
199
  output.append("⚠️ **Risks & Considerations:**")
121
200
  for risk in plan_doc.risks:
122
201
  output.append(f" • {risk}")
123
202
  output.append("")
124
-
203
+
125
204
  # Open questions
126
205
  if plan_doc.open_questions:
127
206
  output.append("❓ **Open Questions:**")
128
207
  for question in plan_doc.open_questions:
129
208
  output.append(f" • {question}")
130
209
  output.append("")
131
-
210
+
132
211
  # References
133
212
  if plan_doc.references:
134
213
  output.append("📚 **References:**")
135
214
  for ref in plan_doc.references:
136
215
  output.append(f" • {ref}")
137
216
  output.append("")
138
-
217
+
139
218
  # Rollback plan
140
219
  if plan_doc.rollback:
141
220
  output.append(f"🔄 **Rollback Plan:** {plan_doc.rollback}")
142
221
  output.append("")
143
-
222
+
144
223
  # Print everything at once
145
224
  await ui.info("\n".join(output))
146
225
 
@@ -148,13 +227,14 @@ class PresentPlanTool(BaseTool):
148
227
  def create_present_plan_tool(state_manager):
149
228
  """
150
229
  Factory function to create present_plan tool with the correct state manager.
151
-
230
+
152
231
  Args:
153
232
  state_manager: The StateManager instance to use
154
-
233
+
155
234
  Returns:
156
235
  Callable: The present_plan function bound to the provided state manager
157
236
  """
237
+
158
238
  async def present_plan(
159
239
  title: str,
160
240
  overview: str,
@@ -170,23 +250,23 @@ def create_present_plan_tool(state_manager):
170
250
  ) -> str:
171
251
  """
172
252
  Present a structured implementation plan for user approval.
173
-
253
+
174
254
  This tool should ONLY be called when you have a complete, well-researched plan.
175
255
  All required sections must be filled out before calling this tool.
176
-
256
+
177
257
  Args:
178
258
  title: Brief, descriptive title for the implementation plan
179
259
  overview: High-level summary of what needs to be implemented and why
180
260
  steps: Ordered list of specific implementation steps (required)
181
261
  files_to_modify: List of existing files that need to be modified
182
- files_to_create: List of new files that need to be created
262
+ files_to_create: List of new files that need to be created
183
263
  risks: Potential risks, challenges, or considerations
184
264
  tests: Testing approach and test cases to validate implementation
185
265
  rollback: Plan for reverting changes if needed
186
266
  open_questions: Any remaining questions or uncertainties
187
267
  success_criteria: Specific criteria for considering the task complete
188
268
  references: External resources, documentation, or research sources
189
-
269
+
190
270
  Returns:
191
271
  str: Status message about plan presentation
192
272
  """
@@ -204,5 +284,5 @@ def create_present_plan_tool(state_manager):
204
284
  success_criteria=success_criteria,
205
285
  references=references,
206
286
  )
207
-
208
- return present_plan
287
+
288
+ return present_plan
@@ -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"
@@ -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)