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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- 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
|
+
)
|