mycode-coding-agent 0.1.0__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.
- mycode/__init__.py +0 -0
- mycode/adapters/__init__.py +21 -0
- mycode/adapters/jsonl.py +692 -0
- mycode/agent/__init__.py +25 -0
- mycode/agent/events.py +111 -0
- mycode/agent/outcome.py +103 -0
- mycode/agent/progress.py +373 -0
- mycode/agent/runner.py +1481 -0
- mycode/application/__init__.py +38 -0
- mycode/application/agent_session.py +367 -0
- mycode/application/events.py +59 -0
- mycode/application/runtime.py +211 -0
- mycode/application/sessions.py +180 -0
- mycode/cli.py +840 -0
- mycode/config.py +355 -0
- mycode/context/__init__.py +1 -0
- mycode/context/artifacts.py +672 -0
- mycode/context/budget.py +752 -0
- mycode/context/builder.py +112 -0
- mycode/context/compact.py +795 -0
- mycode/context/tool_result_format.py +199 -0
- mycode/context/tool_result_retention.py +261 -0
- mycode/conversation.py +78 -0
- mycode/error_handling.py +481 -0
- mycode/event_format.py +147 -0
- mycode/instructions.py +285 -0
- mycode/llm.py +771 -0
- mycode/mcp/__init__.py +41 -0
- mycode/mcp/client.py +44 -0
- mycode/mcp/config.py +207 -0
- mycode/mcp/errors.py +302 -0
- mycode/mcp/manager.py +339 -0
- mycode/mcp/models.py +20 -0
- mycode/mcp/result_adapter.py +58 -0
- mycode/mcp/tool_adapter.py +145 -0
- mycode/mcp/trust.py +313 -0
- mycode/memory.py +570 -0
- mycode/memory_context.py +245 -0
- mycode/messages.py +63 -0
- mycode/observability.py +28 -0
- mycode/permissions.py +262 -0
- mycode/persistence/__init__.py +1 -0
- mycode/persistence/filesystem.py +291 -0
- mycode/persistence/project_storage.py +208 -0
- mycode/persistence/session_lock.py +138 -0
- mycode/persistence/session_store.py +503 -0
- mycode/presentation/__init__.py +1 -0
- mycode/presentation/cli/__init__.py +14 -0
- mycode/presentation/cli/confirmer.py +116 -0
- mycode/presentation/cli/mcp_trust.py +61 -0
- mycode/presentation/cli/presenter.py +320 -0
- mycode/presentation/cli/session_menu.py +146 -0
- mycode/presentation/cli/subagent_observer.py +124 -0
- mycode/presentation/command_format.py +90 -0
- mycode/presentation/commands.py +95 -0
- mycode/presentation/tui/__init__.py +6 -0
- mycode/presentation/tui/app.py +1351 -0
- mycode/presentation/tui/interactions.py +253 -0
- mycode/presentation/tui/presenter.py +266 -0
- mycode/presentation/tui/screens.py +305 -0
- mycode/presentation/tui/widgets.py +214 -0
- mycode/project.py +22 -0
- mycode/prompts.py +181 -0
- mycode/reasoning.py +40 -0
- mycode/session.py +86 -0
- mycode/skills/__init__.py +27 -0
- mycode/skills/builtin/database-recovery/SKILL.md +138 -0
- mycode/skills/builtin/database-recovery/references/sqlite.md +235 -0
- mycode/skills/registry.py +295 -0
- mycode/skills/state.py +68 -0
- mycode/subagents/__init__.py +1 -0
- mycode/subagents/audit.py +212 -0
- mycode/subagents/concurrency.py +124 -0
- mycode/subagents/contracts.py +421 -0
- mycode/subagents/delegate.py +80 -0
- mycode/subagents/delegation.py +128 -0
- mycode/subagents/lifecycle.py +86 -0
- mycode/subagents/limits.py +7 -0
- mycode/subagents/observability.py +150 -0
- mycode/subagents/persistence.py +152 -0
- mycode/subagents/profiles.py +184 -0
- mycode/subagents/prompts.py +67 -0
- mycode/subagents/results.py +178 -0
- mycode/subagents/runtime.py +528 -0
- mycode/subagents/snapshots.py +211 -0
- mycode/subagents/tool_batch.py +260 -0
- mycode/tools/__init__.py +81 -0
- mycode/tools/base.py +222 -0
- mycode/tools/bounds.py +14 -0
- mycode/tools/command_executor.py +167 -0
- mycode/tools/command_output.py +166 -0
- mycode/tools/command_risk.py +596 -0
- mycode/tools/defaults.py +59 -0
- mycode/tools/edit_file.py +524 -0
- mycode/tools/file_mutation.py +30 -0
- mycode/tools/glob.py +247 -0
- mycode/tools/grep.py +324 -0
- mycode/tools/ignore.py +122 -0
- mycode/tools/inspect_changes.py +269 -0
- mycode/tools/load_skill.py +92 -0
- mycode/tools/memory.py +264 -0
- mycode/tools/path_permissions.py +78 -0
- mycode/tools/patterns.py +48 -0
- mycode/tools/permission_metadata.py +27 -0
- mycode/tools/process_tree.py +166 -0
- mycode/tools/read_file.py +242 -0
- mycode/tools/read_skill_resource.py +93 -0
- mycode/tools/registry.py +279 -0
- mycode/tools/run_command.py +237 -0
- mycode/tools/run_skill_script.py +206 -0
- mycode/tools/run_validation.py +107 -0
- mycode/tools/submit_result.py +93 -0
- mycode/tools/text.py +15 -0
- mycode/tools/validation_command.py +377 -0
- mycode/tools/workspace.py +33 -0
- mycode/tools/write_file.py +169 -0
- mycode_coding_agent-0.1.0.dist-info/METADATA +244 -0
- mycode_coding_agent-0.1.0.dist-info/RECORD +121 -0
- mycode_coding_agent-0.1.0.dist-info/WHEEL +4 -0
- mycode_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/tools/glob.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_validator
|
|
4
|
+
|
|
5
|
+
from mycode.permissions import PermissionChecker, PermissionDecision, PermissionRequest
|
|
6
|
+
from mycode.tools.base import PydanticTool, ToolArgs, ToolResult
|
|
7
|
+
from mycode.tools.bounds import clamp_positive_int_upper_bound
|
|
8
|
+
from mycode.tools.ignore import is_ignored_path, is_low_relevance_path
|
|
9
|
+
from mycode.tools.path_permissions import PathPermissionPolicy
|
|
10
|
+
from mycode.tools.patterns import (
|
|
11
|
+
is_explicit_path_pattern,
|
|
12
|
+
is_sensitive_path_pattern,
|
|
13
|
+
validate_relative_pattern,
|
|
14
|
+
)
|
|
15
|
+
from mycode.tools.workspace import Workspace, WorkspacePathError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_MAX_RESULTS = 100
|
|
19
|
+
MAX_RESULTS_LIMIT = 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GlobArgs(ToolArgs):
|
|
23
|
+
pattern: str = Field(
|
|
24
|
+
min_length=1,
|
|
25
|
+
description=(
|
|
26
|
+
"用于匹配 workspace 内文件路径的相对 glob pattern,例如 **/*.py。"
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
max_results: int = Field(
|
|
30
|
+
default=DEFAULT_MAX_RESULTS,
|
|
31
|
+
ge=1,
|
|
32
|
+
le=MAX_RESULTS_LIMIT,
|
|
33
|
+
strict=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@field_validator("max_results", mode="before")
|
|
37
|
+
@classmethod
|
|
38
|
+
def clamp_max_results(cls, value: object) -> object:
|
|
39
|
+
return clamp_positive_int_upper_bound(
|
|
40
|
+
value,
|
|
41
|
+
upper_bound=MAX_RESULTS_LIMIT,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GlobTool(PydanticTool[GlobArgs]):
|
|
46
|
+
name = "glob"
|
|
47
|
+
description = "按 glob pattern 查找 workspace 内文件。"
|
|
48
|
+
args_model = GlobArgs
|
|
49
|
+
capability = "read"
|
|
50
|
+
risk = "low"
|
|
51
|
+
concurrency_safe = True
|
|
52
|
+
|
|
53
|
+
def __init__(self, workspace: Workspace) -> None:
|
|
54
|
+
self.workspace = workspace
|
|
55
|
+
|
|
56
|
+
def build_permission_request(self, args: GlobArgs) -> PermissionRequest:
|
|
57
|
+
return PermissionRequest(
|
|
58
|
+
tool_name=self.name,
|
|
59
|
+
capability=self.capability,
|
|
60
|
+
action=self.name,
|
|
61
|
+
target=args.pattern,
|
|
62
|
+
arguments=args.model_dump(),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def check_permission(
|
|
66
|
+
self,
|
|
67
|
+
args: GlobArgs,
|
|
68
|
+
permission_checker: PermissionChecker,
|
|
69
|
+
) -> tuple[PermissionRequest, PermissionDecision]:
|
|
70
|
+
request = self.build_permission_request(args)
|
|
71
|
+
pattern_error = validate_relative_pattern(
|
|
72
|
+
args.pattern,
|
|
73
|
+
label="Glob pattern",
|
|
74
|
+
)
|
|
75
|
+
if pattern_error is not None:
|
|
76
|
+
return request, PermissionDecision.deny(
|
|
77
|
+
reason="outside_workspace",
|
|
78
|
+
message=pattern_error,
|
|
79
|
+
metadata={"pattern": args.pattern},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if is_explicit_path_pattern(args.pattern):
|
|
83
|
+
path_decision = PathPermissionPolicy(self.workspace).check_path(
|
|
84
|
+
request,
|
|
85
|
+
args.pattern,
|
|
86
|
+
)
|
|
87
|
+
if path_decision.status != "allow":
|
|
88
|
+
return request, path_decision
|
|
89
|
+
|
|
90
|
+
decision = permission_checker.check(
|
|
91
|
+
request,
|
|
92
|
+
self.get_permission_profile(),
|
|
93
|
+
)
|
|
94
|
+
if decision.status != "allow":
|
|
95
|
+
return request, decision
|
|
96
|
+
|
|
97
|
+
return request, PermissionDecision.allow(
|
|
98
|
+
message=decision.message,
|
|
99
|
+
metadata={**decision.metadata, **path_decision.metadata},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if is_sensitive_path_pattern(args.pattern):
|
|
103
|
+
return request, PermissionDecision.ask(
|
|
104
|
+
reason="sensitive_path",
|
|
105
|
+
message=f"Sensitive path pattern requires confirmation: {args.pattern}",
|
|
106
|
+
metadata={
|
|
107
|
+
"pattern": args.pattern,
|
|
108
|
+
"pattern_scope": "sensitive_pattern",
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return request, permission_checker.check(
|
|
113
|
+
request,
|
|
114
|
+
self.get_permission_profile(),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def run_authorized(
|
|
118
|
+
self,
|
|
119
|
+
args: GlobArgs,
|
|
120
|
+
decision: PermissionDecision,
|
|
121
|
+
) -> ToolResult:
|
|
122
|
+
return self._run_with_options(
|
|
123
|
+
args,
|
|
124
|
+
include_ignored=_can_include_ignored(decision),
|
|
125
|
+
include_low_relevance=is_explicit_path_pattern(args.pattern),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _run(self, args: GlobArgs) -> ToolResult:
|
|
129
|
+
return self._run_with_options(
|
|
130
|
+
args,
|
|
131
|
+
include_ignored=False,
|
|
132
|
+
include_low_relevance=is_explicit_path_pattern(args.pattern),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _run_with_options(
|
|
136
|
+
self,
|
|
137
|
+
args: GlobArgs,
|
|
138
|
+
*,
|
|
139
|
+
include_ignored: bool,
|
|
140
|
+
include_low_relevance: bool,
|
|
141
|
+
) -> ToolResult:
|
|
142
|
+
pattern_error = validate_relative_pattern(
|
|
143
|
+
args.pattern,
|
|
144
|
+
label="Glob pattern",
|
|
145
|
+
)
|
|
146
|
+
if pattern_error is not None:
|
|
147
|
+
return ToolResult.failure(
|
|
148
|
+
error=pattern_error,
|
|
149
|
+
metadata={"pattern": args.pattern},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
result = _find_file_matches(
|
|
154
|
+
self.workspace,
|
|
155
|
+
args.pattern,
|
|
156
|
+
include_ignored=include_ignored,
|
|
157
|
+
include_low_relevance=include_low_relevance,
|
|
158
|
+
)
|
|
159
|
+
except ValueError as error:
|
|
160
|
+
return ToolResult.failure(
|
|
161
|
+
error=f"Invalid glob pattern: {args.pattern}",
|
|
162
|
+
metadata={"pattern": args.pattern, "reason": str(error)},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
matches = result.matches
|
|
166
|
+
selected_matches = matches[: args.max_results]
|
|
167
|
+
content = "\n".join(selected_matches)
|
|
168
|
+
if not content:
|
|
169
|
+
content = "No files matched."
|
|
170
|
+
if result.filtered_count > 0:
|
|
171
|
+
content += " Some matches were filtered by workspace relevance or safety rules."
|
|
172
|
+
|
|
173
|
+
return ToolResult.success(
|
|
174
|
+
content=content,
|
|
175
|
+
metadata={
|
|
176
|
+
"pattern": args.pattern,
|
|
177
|
+
"result_count": len(selected_matches),
|
|
178
|
+
"total_matches": len(matches),
|
|
179
|
+
"filtered_count": result.filtered_count,
|
|
180
|
+
"filtered_reasons": result.filtered_reasons,
|
|
181
|
+
"max_results": args.max_results,
|
|
182
|
+
"truncated": len(matches) > args.max_results,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class GlobMatchResult:
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
matches: list[str],
|
|
191
|
+
filtered_reasons: dict[str, int],
|
|
192
|
+
) -> None:
|
|
193
|
+
self.matches = matches
|
|
194
|
+
self.filtered_reasons = filtered_reasons
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def filtered_count(self) -> int:
|
|
198
|
+
return sum(self.filtered_reasons.values())
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _find_file_matches(
|
|
202
|
+
workspace: Workspace,
|
|
203
|
+
pattern: str,
|
|
204
|
+
*,
|
|
205
|
+
include_ignored: bool = False,
|
|
206
|
+
include_low_relevance: bool = False,
|
|
207
|
+
) -> GlobMatchResult:
|
|
208
|
+
matches: list[str] = []
|
|
209
|
+
filtered_reasons: dict[str, int] = {}
|
|
210
|
+
|
|
211
|
+
for candidate in workspace.root.glob(pattern):
|
|
212
|
+
try:
|
|
213
|
+
workspace.resolve_path(candidate)
|
|
214
|
+
if not candidate.is_file():
|
|
215
|
+
continue
|
|
216
|
+
if not include_ignored and is_ignored_path(candidate, workspace.root):
|
|
217
|
+
_increment_reason(filtered_reasons, "ignored")
|
|
218
|
+
continue
|
|
219
|
+
if (
|
|
220
|
+
not include_low_relevance
|
|
221
|
+
and is_low_relevance_path(candidate, workspace.root)
|
|
222
|
+
):
|
|
223
|
+
_increment_reason(filtered_reasons, "low_relevance")
|
|
224
|
+
continue
|
|
225
|
+
except (OSError, WorkspacePathError):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
matches.append(candidate.relative_to(workspace.root).as_posix())
|
|
229
|
+
|
|
230
|
+
return GlobMatchResult(
|
|
231
|
+
matches=sorted(matches),
|
|
232
|
+
filtered_reasons=filtered_reasons,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _increment_reason(reasons: dict[str, int], reason: str) -> None:
|
|
237
|
+
reasons[reason] = reasons.get(reason, 0) + 1
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _can_include_ignored(decision: PermissionDecision) -> bool:
|
|
241
|
+
return (
|
|
242
|
+
decision.metadata.get("confirmation_status") == "approved"
|
|
243
|
+
and (
|
|
244
|
+
decision.metadata.get("path_scope") in {"ignored_path", "sensitive_path"}
|
|
245
|
+
or decision.metadata.get("pattern_scope") == "sensitive_pattern"
|
|
246
|
+
)
|
|
247
|
+
)
|
mycode/tools/grep.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, field_validator
|
|
5
|
+
|
|
6
|
+
from mycode.permissions import PermissionChecker, PermissionDecision, PermissionRequest
|
|
7
|
+
from mycode.tools.base import PydanticTool, ToolArgs, ToolResult
|
|
8
|
+
from mycode.tools.bounds import clamp_positive_int_upper_bound
|
|
9
|
+
from mycode.tools.ignore import is_ignored_path, is_low_relevance_path
|
|
10
|
+
from mycode.tools.path_permissions import PathPermissionPolicy
|
|
11
|
+
from mycode.tools.patterns import (
|
|
12
|
+
is_explicit_path_pattern,
|
|
13
|
+
is_sensitive_path_pattern,
|
|
14
|
+
validate_relative_pattern,
|
|
15
|
+
)
|
|
16
|
+
from mycode.tools.text import contains_nul_byte, decode_text
|
|
17
|
+
from mycode.tools.workspace import Workspace, WorkspacePathError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DEFAULT_PATH_PATTERN = "**/*"
|
|
21
|
+
DEFAULT_MAX_RESULTS = 100
|
|
22
|
+
MAX_RESULTS_LIMIT = 1000
|
|
23
|
+
MAX_MATCH_LINE_CHARS = 240
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GrepArgs(ToolArgs):
|
|
27
|
+
query: str = Field(
|
|
28
|
+
min_length=1,
|
|
29
|
+
description="用于匹配文件内容的正则表达式。",
|
|
30
|
+
)
|
|
31
|
+
path_pattern: str = Field(
|
|
32
|
+
default=DEFAULT_PATH_PATTERN,
|
|
33
|
+
min_length=1,
|
|
34
|
+
description=(
|
|
35
|
+
"限定搜索文件范围的 workspace 相对 glob pattern,例如 **/*.py。"
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
case_sensitive: bool = False
|
|
39
|
+
max_results: int = Field(
|
|
40
|
+
default=DEFAULT_MAX_RESULTS,
|
|
41
|
+
ge=1,
|
|
42
|
+
le=MAX_RESULTS_LIMIT,
|
|
43
|
+
strict=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@field_validator("max_results", mode="before")
|
|
47
|
+
@classmethod
|
|
48
|
+
def clamp_max_results(cls, value: object) -> object:
|
|
49
|
+
return clamp_positive_int_upper_bound(
|
|
50
|
+
value,
|
|
51
|
+
upper_bound=MAX_RESULTS_LIMIT,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GrepTool(PydanticTool[GrepArgs]):
|
|
56
|
+
name = "grep"
|
|
57
|
+
description = "使用正则表达式搜索 workspace 文件内容。"
|
|
58
|
+
args_model = GrepArgs
|
|
59
|
+
capability = "read"
|
|
60
|
+
risk = "low"
|
|
61
|
+
concurrency_safe = True
|
|
62
|
+
|
|
63
|
+
def __init__(self, workspace: Workspace) -> None:
|
|
64
|
+
self.workspace = workspace
|
|
65
|
+
|
|
66
|
+
def build_permission_request(self, args: GrepArgs) -> PermissionRequest:
|
|
67
|
+
return PermissionRequest(
|
|
68
|
+
tool_name=self.name,
|
|
69
|
+
capability=self.capability,
|
|
70
|
+
action=self.name,
|
|
71
|
+
target=args.path_pattern,
|
|
72
|
+
arguments=args.model_dump(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def check_permission(
|
|
76
|
+
self,
|
|
77
|
+
args: GrepArgs,
|
|
78
|
+
permission_checker: PermissionChecker,
|
|
79
|
+
) -> tuple[PermissionRequest, PermissionDecision]:
|
|
80
|
+
request = self.build_permission_request(args)
|
|
81
|
+
pattern_error = validate_relative_pattern(
|
|
82
|
+
args.path_pattern,
|
|
83
|
+
label="Path pattern",
|
|
84
|
+
)
|
|
85
|
+
if pattern_error is not None:
|
|
86
|
+
return request, PermissionDecision.deny(
|
|
87
|
+
reason="outside_workspace",
|
|
88
|
+
message=pattern_error,
|
|
89
|
+
metadata={"path_pattern": args.path_pattern},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if is_explicit_path_pattern(args.path_pattern):
|
|
93
|
+
path_decision = PathPermissionPolicy(self.workspace).check_path(
|
|
94
|
+
request,
|
|
95
|
+
args.path_pattern,
|
|
96
|
+
)
|
|
97
|
+
if path_decision.status != "allow":
|
|
98
|
+
return request, path_decision
|
|
99
|
+
|
|
100
|
+
decision = permission_checker.check(
|
|
101
|
+
request,
|
|
102
|
+
self.get_permission_profile(),
|
|
103
|
+
)
|
|
104
|
+
if decision.status != "allow":
|
|
105
|
+
return request, decision
|
|
106
|
+
|
|
107
|
+
return request, PermissionDecision.allow(
|
|
108
|
+
message=decision.message,
|
|
109
|
+
metadata={**decision.metadata, **path_decision.metadata},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if is_sensitive_path_pattern(args.path_pattern):
|
|
113
|
+
return request, PermissionDecision.ask(
|
|
114
|
+
reason="sensitive_path",
|
|
115
|
+
message=(
|
|
116
|
+
"Sensitive path pattern requires confirmation: "
|
|
117
|
+
f"{args.path_pattern}"
|
|
118
|
+
),
|
|
119
|
+
metadata={
|
|
120
|
+
"path_pattern": args.path_pattern,
|
|
121
|
+
"pattern_scope": "sensitive_pattern",
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return request, permission_checker.check(
|
|
126
|
+
request,
|
|
127
|
+
self.get_permission_profile(),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def run_authorized(
|
|
131
|
+
self,
|
|
132
|
+
args: GrepArgs,
|
|
133
|
+
decision: PermissionDecision,
|
|
134
|
+
) -> ToolResult:
|
|
135
|
+
return self._run_with_options(
|
|
136
|
+
args,
|
|
137
|
+
include_ignored=_can_include_ignored(decision),
|
|
138
|
+
include_low_relevance=is_explicit_path_pattern(args.path_pattern),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _run(self, args: GrepArgs) -> ToolResult:
|
|
142
|
+
return self._run_with_options(
|
|
143
|
+
args,
|
|
144
|
+
include_ignored=False,
|
|
145
|
+
include_low_relevance=is_explicit_path_pattern(args.path_pattern),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _run_with_options(
|
|
149
|
+
self,
|
|
150
|
+
args: GrepArgs,
|
|
151
|
+
*,
|
|
152
|
+
include_ignored: bool,
|
|
153
|
+
include_low_relevance: bool,
|
|
154
|
+
) -> ToolResult:
|
|
155
|
+
pattern_error = validate_relative_pattern(
|
|
156
|
+
args.path_pattern,
|
|
157
|
+
label="Path pattern",
|
|
158
|
+
)
|
|
159
|
+
if pattern_error is not None:
|
|
160
|
+
return ToolResult.failure(
|
|
161
|
+
error=pattern_error,
|
|
162
|
+
metadata={"path_pattern": args.path_pattern},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
flags = 0 if args.case_sensitive else re.IGNORECASE
|
|
166
|
+
try:
|
|
167
|
+
query_pattern = re.compile(args.query, flags=flags)
|
|
168
|
+
except re.error as error:
|
|
169
|
+
return ToolResult.failure(
|
|
170
|
+
error=f"Invalid regular expression: {error}",
|
|
171
|
+
metadata={"query": args.query, "reason": str(error)},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
search_result = _find_search_files(
|
|
176
|
+
self.workspace,
|
|
177
|
+
args.path_pattern,
|
|
178
|
+
include_ignored=include_ignored,
|
|
179
|
+
include_low_relevance=include_low_relevance,
|
|
180
|
+
)
|
|
181
|
+
except ValueError as error:
|
|
182
|
+
return ToolResult.failure(
|
|
183
|
+
error=f"Invalid path pattern: {args.path_pattern}",
|
|
184
|
+
metadata={"path_pattern": args.path_pattern, "reason": str(error)},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
files = search_result.files
|
|
188
|
+
matches: list[str] = []
|
|
189
|
+
searched_files = 0
|
|
190
|
+
skipped_files = 0
|
|
191
|
+
truncated = False
|
|
192
|
+
|
|
193
|
+
for path in files:
|
|
194
|
+
raw_content = _read_bytes(path)
|
|
195
|
+
if raw_content is None or contains_nul_byte(raw_content):
|
|
196
|
+
skipped_files += 1
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
decoded = decode_text(raw_content)
|
|
200
|
+
if decoded is None:
|
|
201
|
+
skipped_files += 1
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
text, _encoding = decoded
|
|
205
|
+
searched_files += 1
|
|
206
|
+
relative_path = path.relative_to(self.workspace.root).as_posix()
|
|
207
|
+
|
|
208
|
+
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
209
|
+
if not _line_matches(line, query_pattern):
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if len(matches) >= args.max_results:
|
|
213
|
+
truncated = True
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
matches.append(_format_match(relative_path, line_number, line))
|
|
217
|
+
|
|
218
|
+
if truncated:
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
content = "\n".join(matches)
|
|
222
|
+
if not content:
|
|
223
|
+
content = "No matches found."
|
|
224
|
+
|
|
225
|
+
return ToolResult.success(
|
|
226
|
+
content=content,
|
|
227
|
+
metadata={
|
|
228
|
+
"query": args.query,
|
|
229
|
+
"path_pattern": args.path_pattern,
|
|
230
|
+
"case_sensitive": args.case_sensitive,
|
|
231
|
+
"result_count": len(matches),
|
|
232
|
+
"max_results": args.max_results,
|
|
233
|
+
"truncated": truncated,
|
|
234
|
+
"searched_files": searched_files,
|
|
235
|
+
"skipped_files": skipped_files,
|
|
236
|
+
"filtered_count": search_result.filtered_count,
|
|
237
|
+
"filtered_reasons": search_result.filtered_reasons,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class GrepSearchResult:
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
files: list[Path],
|
|
246
|
+
filtered_reasons: dict[str, int],
|
|
247
|
+
) -> None:
|
|
248
|
+
self.files = files
|
|
249
|
+
self.filtered_reasons = filtered_reasons
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def filtered_count(self) -> int:
|
|
253
|
+
return sum(self.filtered_reasons.values())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _find_search_files(
|
|
257
|
+
workspace: Workspace,
|
|
258
|
+
path_pattern: str,
|
|
259
|
+
*,
|
|
260
|
+
include_ignored: bool = False,
|
|
261
|
+
include_low_relevance: bool = False,
|
|
262
|
+
) -> GrepSearchResult:
|
|
263
|
+
files: list[Path] = []
|
|
264
|
+
filtered_reasons: dict[str, int] = {}
|
|
265
|
+
|
|
266
|
+
for candidate in workspace.root.glob(path_pattern):
|
|
267
|
+
try:
|
|
268
|
+
workspace.resolve_path(candidate)
|
|
269
|
+
if not candidate.is_file():
|
|
270
|
+
continue
|
|
271
|
+
if not include_ignored and is_ignored_path(candidate, workspace.root):
|
|
272
|
+
_increment_reason(filtered_reasons, "ignored")
|
|
273
|
+
continue
|
|
274
|
+
if (
|
|
275
|
+
not include_low_relevance
|
|
276
|
+
and is_low_relevance_path(candidate, workspace.root)
|
|
277
|
+
):
|
|
278
|
+
_increment_reason(filtered_reasons, "low_relevance")
|
|
279
|
+
continue
|
|
280
|
+
except (OSError, WorkspacePathError):
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
files.append(candidate)
|
|
284
|
+
|
|
285
|
+
return GrepSearchResult(
|
|
286
|
+
files=sorted(files),
|
|
287
|
+
filtered_reasons=filtered_reasons,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _increment_reason(reasons: dict[str, int], reason: str) -> None:
|
|
292
|
+
reasons[reason] = reasons.get(reason, 0) + 1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _can_include_ignored(decision: PermissionDecision) -> bool:
|
|
296
|
+
return (
|
|
297
|
+
decision.metadata.get("confirmation_status") == "approved"
|
|
298
|
+
and (
|
|
299
|
+
decision.metadata.get("path_scope") in {"ignored_path", "sensitive_path"}
|
|
300
|
+
or decision.metadata.get("pattern_scope") == "sensitive_pattern"
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _read_bytes(path: Path) -> bytes | None:
|
|
306
|
+
try:
|
|
307
|
+
return path.read_bytes()
|
|
308
|
+
except OSError:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _line_matches(line: str, query_pattern: re.Pattern[str]) -> bool:
|
|
313
|
+
return query_pattern.search(line) is not None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _format_match(path: str, line_number: int, line: str) -> str:
|
|
317
|
+
return f"{path}:{line_number} | {_truncate_line(line)}"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _truncate_line(line: str) -> str:
|
|
321
|
+
if len(line) <= MAX_MATCH_LINE_CHARS:
|
|
322
|
+
return line
|
|
323
|
+
|
|
324
|
+
return f"{line[: MAX_MATCH_LINE_CHARS - 3]}..."
|
mycode/tools/ignore.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DEFAULT_EXCLUDED_DIR_NAMES = frozenset(
|
|
5
|
+
{
|
|
6
|
+
".git",
|
|
7
|
+
".mypy_cache",
|
|
8
|
+
".pytest_cache",
|
|
9
|
+
".ruff_cache",
|
|
10
|
+
".venv",
|
|
11
|
+
"__pycache__",
|
|
12
|
+
"build",
|
|
13
|
+
"dist",
|
|
14
|
+
"node_modules",
|
|
15
|
+
"venv",
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOW_RELEVANCE_DIR_NAMES = frozenset(
|
|
20
|
+
{
|
|
21
|
+
"__fixtures__",
|
|
22
|
+
"__snapshots__",
|
|
23
|
+
"archive",
|
|
24
|
+
"archives",
|
|
25
|
+
"demo",
|
|
26
|
+
"demos",
|
|
27
|
+
"example",
|
|
28
|
+
"examples",
|
|
29
|
+
"external",
|
|
30
|
+
"fixture",
|
|
31
|
+
"fixtures",
|
|
32
|
+
"reference",
|
|
33
|
+
"references",
|
|
34
|
+
"sample",
|
|
35
|
+
"samples",
|
|
36
|
+
"snapshot",
|
|
37
|
+
"snapshots",
|
|
38
|
+
"third_party",
|
|
39
|
+
"vendor",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
DEFAULT_SENSITIVE_FILE_NAMES = frozenset(
|
|
44
|
+
{
|
|
45
|
+
".env",
|
|
46
|
+
".envrc",
|
|
47
|
+
".npmrc",
|
|
48
|
+
".pypirc",
|
|
49
|
+
"id_dsa",
|
|
50
|
+
"id_ecdsa",
|
|
51
|
+
"id_ed25519",
|
|
52
|
+
"id_rsa",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
DEFAULT_SENSITIVE_FILE_SUFFIXES = (
|
|
57
|
+
".key",
|
|
58
|
+
".p12",
|
|
59
|
+
".pem",
|
|
60
|
+
".pfx",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
DEFAULT_SAFE_ENV_TEMPLATE_FILE_NAMES = frozenset(
|
|
64
|
+
{
|
|
65
|
+
".env.example",
|
|
66
|
+
".env.sample",
|
|
67
|
+
".env.template",
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_ignored_path(
|
|
73
|
+
path: Path,
|
|
74
|
+
root: Path,
|
|
75
|
+
excluded_dir_names: frozenset[str] = DEFAULT_EXCLUDED_DIR_NAMES,
|
|
76
|
+
) -> bool:
|
|
77
|
+
try:
|
|
78
|
+
relative_path = path.relative_to(root)
|
|
79
|
+
except ValueError:
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
if any(part in excluded_dir_names for part in relative_path.parts[:-1]):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
return is_sensitive_path(path, root)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_low_relevance_path(
|
|
89
|
+
path: Path,
|
|
90
|
+
root: Path,
|
|
91
|
+
low_relevance_dir_names: frozenset[str] = DEFAULT_LOW_RELEVANCE_DIR_NAMES,
|
|
92
|
+
) -> bool:
|
|
93
|
+
try:
|
|
94
|
+
relative_path = path.relative_to(root)
|
|
95
|
+
except ValueError:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
return any(part.casefold() in low_relevance_dir_names for part in relative_path.parts[:-1])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_sensitive_path(
|
|
102
|
+
path: Path,
|
|
103
|
+
root: Path,
|
|
104
|
+
sensitive_file_names: frozenset[str] = DEFAULT_SENSITIVE_FILE_NAMES,
|
|
105
|
+
sensitive_file_suffixes: tuple[str, ...] = DEFAULT_SENSITIVE_FILE_SUFFIXES,
|
|
106
|
+
) -> bool:
|
|
107
|
+
try:
|
|
108
|
+
path.relative_to(root)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
name = path.name.casefold()
|
|
113
|
+
if name in DEFAULT_SAFE_ENV_TEMPLATE_FILE_NAMES:
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
if name in sensitive_file_names:
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
if name.startswith(".env."):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
return any(name.endswith(suffix) for suffix in sensitive_file_suffixes)
|