emdash-core 0.1.7__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.
Files changed (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,131 @@
1
+ """Base classes for agent tools."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any, Optional
7
+
8
+
9
+ class ToolCategory(Enum):
10
+ """Categories of agent tools."""
11
+
12
+ SEARCH = "search"
13
+ TRAVERSAL = "traversal"
14
+ ANALYTICS = "analytics"
15
+ HISTORY = "history"
16
+ PLANNING = "planning"
17
+
18
+
19
+ @dataclass
20
+ class ToolResult:
21
+ """Standardized result from any tool execution."""
22
+
23
+ success: bool
24
+ data: dict = field(default_factory=dict)
25
+ error: Optional[str] = None
26
+ suggestions: list[str] = field(default_factory=list)
27
+ metadata: dict = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict:
30
+ """Convert to dictionary for JSON serialization."""
31
+ return {
32
+ "success": self.success,
33
+ "data": self.data,
34
+ "error": self.error,
35
+ "suggestions": self.suggestions,
36
+ "metadata": self.metadata,
37
+ }
38
+
39
+ @classmethod
40
+ def success_result(
41
+ cls,
42
+ data: dict,
43
+ suggestions: list[str] = None,
44
+ metadata: dict = None,
45
+ ) -> "ToolResult":
46
+ """Create a successful result."""
47
+ return cls(
48
+ success=True,
49
+ data=data,
50
+ suggestions=suggestions or [],
51
+ metadata=metadata or {},
52
+ )
53
+
54
+ @classmethod
55
+ def error_result(
56
+ cls,
57
+ error: str,
58
+ suggestions: list[str] = None,
59
+ ) -> "ToolResult":
60
+ """Create an error result."""
61
+ return cls(
62
+ success=False,
63
+ error=error,
64
+ suggestions=suggestions or [],
65
+ )
66
+
67
+
68
+ class BaseTool(ABC):
69
+ """Abstract base class for all agent tools."""
70
+
71
+ name: str = ""
72
+ description: str = ""
73
+ category: ToolCategory = ToolCategory.SEARCH
74
+
75
+ def __init__(self, connection=None):
76
+ """Initialize the tool.
77
+
78
+ Args:
79
+ connection: Kuzu connection. If None, uses global read-only connection.
80
+ """
81
+ from ...graph.connection import get_read_connection
82
+
83
+ self.connection = connection or get_read_connection()
84
+
85
+ @abstractmethod
86
+ def execute(self, **kwargs) -> ToolResult:
87
+ """Execute the tool with given parameters.
88
+
89
+ Args:
90
+ **kwargs: Tool-specific parameters
91
+
92
+ Returns:
93
+ ToolResult with success/data or error
94
+ """
95
+ pass
96
+
97
+ @abstractmethod
98
+ def get_schema(self) -> dict:
99
+ """Return OpenAI function calling schema.
100
+
101
+ Returns:
102
+ Dict with name, description, and parameters following OpenAI format
103
+ """
104
+ pass
105
+
106
+ def _make_schema(
107
+ self,
108
+ properties: dict,
109
+ required: list[str] = None,
110
+ ) -> dict:
111
+ """Helper to construct OpenAI function schema.
112
+
113
+ Args:
114
+ properties: Parameter properties dict
115
+ required: List of required parameter names
116
+
117
+ Returns:
118
+ Complete OpenAI function schema with type wrapper
119
+ """
120
+ return {
121
+ "type": "function",
122
+ "function": {
123
+ "name": self.name,
124
+ "description": self.description,
125
+ "parameters": {
126
+ "type": "object",
127
+ "properties": properties,
128
+ "required": required or [],
129
+ },
130
+ },
131
+ }
@@ -0,0 +1,484 @@
1
+ """Coding tools for file operations."""
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from .base import BaseTool, ToolResult, ToolCategory
9
+ from ...utils.logger import log
10
+
11
+
12
+ class CodingTool(BaseTool):
13
+ """Base class for coding tools that operate on files."""
14
+
15
+ category = ToolCategory.PLANNING # File ops are part of planning/coding workflow
16
+
17
+ def __init__(self, repo_root: Path, connection=None):
18
+ """Initialize with repo root for path validation.
19
+
20
+ Args:
21
+ repo_root: Root directory of the repository
22
+ connection: Optional connection (not used for file ops)
23
+ """
24
+ self.repo_root = repo_root.resolve()
25
+ self.connection = connection
26
+
27
+ def _validate_path(self, path: str) -> tuple[bool, str, Optional[Path]]:
28
+ """Validate that a path is within the repo root.
29
+
30
+ Args:
31
+ path: Path to validate
32
+
33
+ Returns:
34
+ Tuple of (is_valid, error_message, resolved_path)
35
+ """
36
+ try:
37
+ # Handle relative and absolute paths
38
+ if os.path.isabs(path):
39
+ full_path = Path(path).resolve()
40
+ else:
41
+ full_path = (self.repo_root / path).resolve()
42
+
43
+ # Check if within repo
44
+ try:
45
+ full_path.relative_to(self.repo_root)
46
+ except ValueError:
47
+ return False, f"Path {path} is outside repository", None
48
+
49
+ return True, "", full_path
50
+
51
+ except Exception as e:
52
+ return False, f"Invalid path: {e}", None
53
+
54
+
55
+ class ReadFileTool(CodingTool):
56
+ """Read the contents of a file."""
57
+
58
+ name = "read_file"
59
+ description = """Read the contents of a file.
60
+ Returns the file content as text."""
61
+
62
+ def execute(
63
+ self,
64
+ path: str,
65
+ start_line: Optional[int] = None,
66
+ end_line: Optional[int] = None,
67
+ ) -> ToolResult:
68
+ """Read a file.
69
+
70
+ Args:
71
+ path: Path to the file
72
+ start_line: Optional starting line (1-indexed)
73
+ end_line: Optional ending line (1-indexed)
74
+
75
+ Returns:
76
+ ToolResult with file content
77
+ """
78
+ valid, error, full_path = self._validate_path(path)
79
+ if not valid:
80
+ return ToolResult.error_result(error)
81
+
82
+ if not full_path.exists():
83
+ return ToolResult.error_result(f"File not found: {path}")
84
+
85
+ if not full_path.is_file():
86
+ return ToolResult.error_result(f"Not a file: {path}")
87
+
88
+ try:
89
+ content = full_path.read_text()
90
+ lines = content.split("\n")
91
+
92
+ # Handle line ranges
93
+ if start_line or end_line:
94
+ start_idx = (start_line - 1) if start_line else 0
95
+ end_idx = end_line if end_line else len(lines)
96
+ lines = lines[start_idx:end_idx]
97
+ content = "\n".join(lines)
98
+
99
+ return ToolResult.success_result(
100
+ data={
101
+ "path": path,
102
+ "content": content,
103
+ "line_count": len(lines),
104
+ },
105
+ )
106
+
107
+ except Exception as e:
108
+ return ToolResult.error_result(f"Failed to read file: {e}")
109
+
110
+ def get_schema(self) -> dict:
111
+ """Get OpenAI function schema."""
112
+ return self._make_schema(
113
+ properties={
114
+ "path": {
115
+ "type": "string",
116
+ "description": "Path to the file to read",
117
+ },
118
+ "start_line": {
119
+ "type": "integer",
120
+ "description": "Starting line number (1-indexed)",
121
+ },
122
+ "end_line": {
123
+ "type": "integer",
124
+ "description": "Ending line number (1-indexed)",
125
+ },
126
+ },
127
+ required=["path"],
128
+ )
129
+
130
+
131
+ class WriteToFileTool(CodingTool):
132
+ """Write content to a file."""
133
+
134
+ name = "write_to_file"
135
+ description = """Write content to a file.
136
+ Creates the file if it doesn't exist, or overwrites if it does."""
137
+
138
+ def execute(
139
+ self,
140
+ path: str,
141
+ content: str,
142
+ ) -> ToolResult:
143
+ """Write to a file.
144
+
145
+ Args:
146
+ path: Path to the file
147
+ content: Content to write
148
+
149
+ Returns:
150
+ ToolResult indicating success
151
+ """
152
+ valid, error, full_path = self._validate_path(path)
153
+ if not valid:
154
+ return ToolResult.error_result(error)
155
+
156
+ try:
157
+ # Create parent directories
158
+ full_path.parent.mkdir(parents=True, exist_ok=True)
159
+
160
+ # Write content
161
+ full_path.write_text(content)
162
+
163
+ return ToolResult.success_result(
164
+ data={
165
+ "path": path,
166
+ "bytes_written": len(content),
167
+ "lines_written": content.count("\n") + 1,
168
+ },
169
+ )
170
+
171
+ except Exception as e:
172
+ return ToolResult.error_result(f"Failed to write file: {e}")
173
+
174
+ def get_schema(self) -> dict:
175
+ """Get OpenAI function schema."""
176
+ return self._make_schema(
177
+ properties={
178
+ "path": {
179
+ "type": "string",
180
+ "description": "Path to the file to write",
181
+ },
182
+ "content": {
183
+ "type": "string",
184
+ "description": "Content to write to the file",
185
+ },
186
+ },
187
+ required=["path", "content"],
188
+ )
189
+
190
+
191
+ class ApplyDiffTool(CodingTool):
192
+ """Apply a diff/patch to a file."""
193
+
194
+ name = "apply_diff"
195
+ description = """Apply a unified diff to a file.
196
+ The diff should be in standard unified diff format."""
197
+
198
+ def execute(
199
+ self,
200
+ path: str,
201
+ diff: str,
202
+ ) -> ToolResult:
203
+ """Apply a diff to a file.
204
+
205
+ Args:
206
+ path: Path to the file
207
+ diff: Unified diff content
208
+
209
+ Returns:
210
+ ToolResult indicating success
211
+ """
212
+ valid, error, full_path = self._validate_path(path)
213
+ if not valid:
214
+ return ToolResult.error_result(error)
215
+
216
+ if not full_path.exists():
217
+ return ToolResult.error_result(f"File not found: {path}")
218
+
219
+ try:
220
+ # Try to apply with patch command
221
+ result = subprocess.run(
222
+ ["patch", "-p0", "--forward"],
223
+ input=diff,
224
+ capture_output=True,
225
+ text=True,
226
+ cwd=self.repo_root,
227
+ timeout=30,
228
+ )
229
+
230
+ if result.returncode != 0:
231
+ # Try with -p1
232
+ result = subprocess.run(
233
+ ["patch", "-p1", "--forward"],
234
+ input=diff,
235
+ capture_output=True,
236
+ text=True,
237
+ cwd=self.repo_root,
238
+ timeout=30,
239
+ )
240
+
241
+ if result.returncode != 0:
242
+ return ToolResult.error_result(
243
+ f"Patch failed: {result.stderr}",
244
+ suggestions=["Check the diff format", "Ensure the file matches the diff context"],
245
+ )
246
+
247
+ return ToolResult.success_result(
248
+ data={
249
+ "path": path,
250
+ "output": result.stdout,
251
+ },
252
+ )
253
+
254
+ except FileNotFoundError:
255
+ return ToolResult.error_result(
256
+ "patch command not found",
257
+ suggestions=["Install patch: brew install gpatch"],
258
+ )
259
+ except subprocess.TimeoutExpired:
260
+ return ToolResult.error_result("Patch timed out")
261
+ except Exception as e:
262
+ return ToolResult.error_result(f"Failed to apply diff: {e}")
263
+
264
+ def get_schema(self) -> dict:
265
+ """Get OpenAI function schema."""
266
+ return self._make_schema(
267
+ properties={
268
+ "path": {
269
+ "type": "string",
270
+ "description": "Path to the file to patch",
271
+ },
272
+ "diff": {
273
+ "type": "string",
274
+ "description": "Unified diff content to apply",
275
+ },
276
+ },
277
+ required=["path", "diff"],
278
+ )
279
+
280
+
281
+ class DeleteFileTool(CodingTool):
282
+ """Delete a file."""
283
+
284
+ name = "delete_file"
285
+ description = """Delete a file from the repository.
286
+ Use with caution - this cannot be undone."""
287
+
288
+ def execute(self, path: str) -> ToolResult:
289
+ """Delete a file.
290
+
291
+ Args:
292
+ path: Path to the file
293
+
294
+ Returns:
295
+ ToolResult indicating success
296
+ """
297
+ valid, error, full_path = self._validate_path(path)
298
+ if not valid:
299
+ return ToolResult.error_result(error)
300
+
301
+ if not full_path.exists():
302
+ return ToolResult.error_result(f"File not found: {path}")
303
+
304
+ if not full_path.is_file():
305
+ return ToolResult.error_result(f"Not a file: {path}")
306
+
307
+ try:
308
+ full_path.unlink()
309
+
310
+ return ToolResult.success_result(
311
+ data={
312
+ "path": path,
313
+ "deleted": True,
314
+ },
315
+ )
316
+
317
+ except Exception as e:
318
+ return ToolResult.error_result(f"Failed to delete file: {e}")
319
+
320
+ def get_schema(self) -> dict:
321
+ """Get OpenAI function schema."""
322
+ return self._make_schema(
323
+ properties={
324
+ "path": {
325
+ "type": "string",
326
+ "description": "Path to the file to delete",
327
+ },
328
+ },
329
+ required=["path"],
330
+ )
331
+
332
+
333
+ class ListFilesTool(CodingTool):
334
+ """List files in a directory."""
335
+
336
+ name = "list_files"
337
+ description = """List files in a directory.
338
+ Can filter by pattern and recurse into subdirectories."""
339
+
340
+ def execute(
341
+ self,
342
+ path: str = ".",
343
+ pattern: Optional[str] = None,
344
+ recursive: bool = False,
345
+ ) -> ToolResult:
346
+ """List files in a directory.
347
+
348
+ Args:
349
+ path: Directory path
350
+ pattern: Optional glob pattern
351
+ recursive: Whether to recurse
352
+
353
+ Returns:
354
+ ToolResult with file list
355
+ """
356
+ valid, error, full_path = self._validate_path(path)
357
+ if not valid:
358
+ return ToolResult.error_result(error)
359
+
360
+ if not full_path.exists():
361
+ return ToolResult.error_result(f"Directory not found: {path}")
362
+
363
+ if not full_path.is_dir():
364
+ return ToolResult.error_result(f"Not a directory: {path}")
365
+
366
+ try:
367
+ files = []
368
+ glob_pattern = pattern or "*"
369
+
370
+ if recursive:
371
+ matches = full_path.rglob(glob_pattern)
372
+ else:
373
+ matches = full_path.glob(glob_pattern)
374
+
375
+ for match in matches:
376
+ if match.is_file():
377
+ # Get relative path from repo root
378
+ rel_path = match.relative_to(self.repo_root)
379
+ files.append({
380
+ "path": str(rel_path),
381
+ "size": match.stat().st_size,
382
+ })
383
+
384
+ # Sort by path
385
+ files.sort(key=lambda x: x["path"])
386
+
387
+ return ToolResult.success_result(
388
+ data={
389
+ "directory": path,
390
+ "files": files[:1000], # Limit results
391
+ "count": len(files),
392
+ "truncated": len(files) > 1000,
393
+ },
394
+ )
395
+
396
+ except Exception as e:
397
+ return ToolResult.error_result(f"Failed to list files: {e}")
398
+
399
+ def get_schema(self) -> dict:
400
+ """Get OpenAI function schema."""
401
+ return self._make_schema(
402
+ properties={
403
+ "path": {
404
+ "type": "string",
405
+ "description": "Directory path (default: current directory)",
406
+ "default": ".",
407
+ },
408
+ "pattern": {
409
+ "type": "string",
410
+ "description": "Glob pattern to filter files",
411
+ },
412
+ "recursive": {
413
+ "type": "boolean",
414
+ "description": "Recurse into subdirectories",
415
+ "default": False,
416
+ },
417
+ },
418
+ required=[],
419
+ )
420
+
421
+
422
+ class ExecuteCommandTool(CodingTool):
423
+ """Execute a shell command."""
424
+
425
+ name = "execute_command"
426
+ description = """Execute a shell command in the repository.
427
+ Commands are run from the repository root."""
428
+
429
+ def execute(
430
+ self,
431
+ command: str,
432
+ timeout: int = 60,
433
+ ) -> ToolResult:
434
+ """Execute a command.
435
+
436
+ Args:
437
+ command: Command to execute
438
+ timeout: Timeout in seconds
439
+
440
+ Returns:
441
+ ToolResult with command output
442
+ """
443
+ try:
444
+ result = subprocess.run(
445
+ command,
446
+ shell=True,
447
+ capture_output=True,
448
+ text=True,
449
+ cwd=self.repo_root,
450
+ timeout=timeout,
451
+ )
452
+
453
+ return ToolResult.success_result(
454
+ data={
455
+ "command": command,
456
+ "exit_code": result.returncode,
457
+ "stdout": result.stdout[-10000:] if result.stdout else "",
458
+ "stderr": result.stderr[-5000:] if result.stderr else "",
459
+ },
460
+ )
461
+
462
+ except subprocess.TimeoutExpired:
463
+ return ToolResult.error_result(
464
+ f"Command timed out after {timeout}s",
465
+ )
466
+ except Exception as e:
467
+ return ToolResult.error_result(f"Command failed: {e}")
468
+
469
+ def get_schema(self) -> dict:
470
+ """Get OpenAI function schema."""
471
+ return self._make_schema(
472
+ properties={
473
+ "command": {
474
+ "type": "string",
475
+ "description": "Shell command to execute",
476
+ },
477
+ "timeout": {
478
+ "type": "integer",
479
+ "description": "Timeout in seconds",
480
+ "default": 60,
481
+ },
482
+ },
483
+ required=["command"],
484
+ )