tunacode-cli 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern matching functionality for the grep tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from .search_result import SearchConfig, SearchResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MatchLike(Protocol):
|
|
13
|
+
def start(self) -> int: ...
|
|
14
|
+
|
|
15
|
+
def end(self) -> int: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SimpleMatch:
|
|
19
|
+
"""Simple match object for non-regex searches."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, start_pos: int, end_pos: int):
|
|
22
|
+
self._start = start_pos
|
|
23
|
+
self._end = end_pos
|
|
24
|
+
|
|
25
|
+
def start(self) -> int:
|
|
26
|
+
return self._start
|
|
27
|
+
|
|
28
|
+
def end(self) -> int:
|
|
29
|
+
return self._end
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PatternMatcher:
|
|
33
|
+
"""Handles pattern matching and relevance scoring for search results."""
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def search_file(
|
|
37
|
+
file_path: Path,
|
|
38
|
+
pattern: str,
|
|
39
|
+
regex_pattern: re.Pattern | None,
|
|
40
|
+
config: SearchConfig,
|
|
41
|
+
) -> list[SearchResult]:
|
|
42
|
+
"""Search a single file for the pattern."""
|
|
43
|
+
try:
|
|
44
|
+
with file_path.open("r", encoding="utf-8", errors="ignore") as f:
|
|
45
|
+
lines = f.readlines()
|
|
46
|
+
|
|
47
|
+
results: list[SearchResult] = []
|
|
48
|
+
for i, line in enumerate(lines):
|
|
49
|
+
line = line.rstrip("\n\r")
|
|
50
|
+
|
|
51
|
+
# Search for pattern
|
|
52
|
+
if regex_pattern:
|
|
53
|
+
matches: list[MatchLike] = list(regex_pattern.finditer(line))
|
|
54
|
+
else:
|
|
55
|
+
# Simple string search
|
|
56
|
+
search_line = line if config.case_sensitive else line.lower()
|
|
57
|
+
search_pattern = pattern if config.case_sensitive else pattern.lower()
|
|
58
|
+
|
|
59
|
+
matches = []
|
|
60
|
+
start = 0
|
|
61
|
+
while True:
|
|
62
|
+
pos = search_line.find(search_pattern, start)
|
|
63
|
+
if pos == -1:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
match = SimpleMatch(pos, pos + len(search_pattern))
|
|
67
|
+
matches.append(match)
|
|
68
|
+
start = pos + 1
|
|
69
|
+
|
|
70
|
+
# Create results for each match
|
|
71
|
+
for match in matches:
|
|
72
|
+
# Get context lines
|
|
73
|
+
context_start = max(0, i - config.context_lines)
|
|
74
|
+
context_end = min(len(lines), i + config.context_lines + 1)
|
|
75
|
+
|
|
76
|
+
context_before = [lines[j].rstrip("\n\r") for j in range(context_start, i)]
|
|
77
|
+
context_after = [lines[j].rstrip("\n\r") for j in range(i + 1, context_end)]
|
|
78
|
+
|
|
79
|
+
# Calculate relevance score
|
|
80
|
+
relevance = PatternMatcher.calculate_relevance(
|
|
81
|
+
str(file_path), line, pattern, match
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result = SearchResult(
|
|
85
|
+
file_path=str(file_path),
|
|
86
|
+
line_number=i + 1,
|
|
87
|
+
line_content=line,
|
|
88
|
+
match_start=match.start(),
|
|
89
|
+
match_end=match.end(),
|
|
90
|
+
context_before=context_before,
|
|
91
|
+
context_after=context_after,
|
|
92
|
+
relevance_score=relevance,
|
|
93
|
+
)
|
|
94
|
+
results.append(result)
|
|
95
|
+
|
|
96
|
+
return results
|
|
97
|
+
|
|
98
|
+
except Exception:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def calculate_relevance(file_path: str, line: str, pattern: str, match: MatchLike) -> float:
|
|
103
|
+
"""Calculate relevance score for a search result."""
|
|
104
|
+
score = 0.0
|
|
105
|
+
|
|
106
|
+
# Base score
|
|
107
|
+
score += 1.0
|
|
108
|
+
|
|
109
|
+
# Boost for exact matches
|
|
110
|
+
if pattern.lower() in line.lower():
|
|
111
|
+
score += 0.5
|
|
112
|
+
|
|
113
|
+
# Boost for matches at word boundaries
|
|
114
|
+
if match.start() == 0 or not line[match.start() - 1].isalnum():
|
|
115
|
+
score += 0.3
|
|
116
|
+
|
|
117
|
+
# Boost for certain file types
|
|
118
|
+
if file_path.endswith((".py", ".js", ".ts", ".java", ".cpp", ".c")):
|
|
119
|
+
score += 0.2
|
|
120
|
+
|
|
121
|
+
# Boost for matches in comments or docstrings
|
|
122
|
+
stripped_line = line.strip()
|
|
123
|
+
if stripped_line.startswith(("#", "//", "/*", '"""', "'''")):
|
|
124
|
+
score += 0.1
|
|
125
|
+
|
|
126
|
+
return score
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def parse_ripgrep_output(output: str) -> list[SearchResult]:
|
|
130
|
+
"""Parse ripgrep JSON output into SearchResult objects."""
|
|
131
|
+
import json
|
|
132
|
+
|
|
133
|
+
results = []
|
|
134
|
+
for line in output.strip().split("\n"):
|
|
135
|
+
if not line:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
data = json.loads(line)
|
|
140
|
+
if data.get("type") != "match":
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
match_data = data["data"]
|
|
144
|
+
result = SearchResult(
|
|
145
|
+
file_path=match_data["path"]["text"],
|
|
146
|
+
line_number=match_data["line_number"],
|
|
147
|
+
line_content=match_data["lines"]["text"].rstrip("\n\r"),
|
|
148
|
+
match_start=match_data["submatches"][0]["start"],
|
|
149
|
+
match_end=match_data["submatches"][0]["end"],
|
|
150
|
+
context_before=[], # Ripgrep context handling would go here
|
|
151
|
+
context_after=[],
|
|
152
|
+
relevance_score=1.0,
|
|
153
|
+
)
|
|
154
|
+
results.append(result)
|
|
155
|
+
except (json.JSONDecodeError, KeyError):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
return results
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extended result formatter with multiple output modes for flexible presentation.
|
|
3
|
+
Result formatting functionality for the grep tool.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .search_result import SearchConfig, SearchResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResultFormatter:
|
|
10
|
+
"""Handles formatting of search results for display with multiple output modes."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def format_results(
|
|
14
|
+
results: list[SearchResult],
|
|
15
|
+
pattern: str,
|
|
16
|
+
config: SearchConfig,
|
|
17
|
+
output_mode: str = "content",
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Format search results for display.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
results: List of search results
|
|
23
|
+
pattern: Search pattern
|
|
24
|
+
config: Search configuration
|
|
25
|
+
output_mode: Output format mode:
|
|
26
|
+
- "content": Show matching lines with context (default)
|
|
27
|
+
- "files_with_matches": Show only file paths
|
|
28
|
+
- "count": Show match counts per file
|
|
29
|
+
- "json": JSON format for programmatic use
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted string based on output mode
|
|
33
|
+
"""
|
|
34
|
+
if not results:
|
|
35
|
+
return f"No matches found for pattern: {pattern}"
|
|
36
|
+
|
|
37
|
+
if output_mode == "files_with_matches":
|
|
38
|
+
return ResultFormatter._format_files_only(results, pattern)
|
|
39
|
+
elif output_mode == "count":
|
|
40
|
+
return ResultFormatter._format_count(results, pattern)
|
|
41
|
+
elif output_mode == "json":
|
|
42
|
+
return ResultFormatter._format_json(results, pattern)
|
|
43
|
+
else: # Default to "content"
|
|
44
|
+
return ResultFormatter._format_content(results, pattern, config)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _format_content(results: list[SearchResult], pattern: str, config: SearchConfig) -> str:
|
|
48
|
+
"""Format results with content grouped by file."""
|
|
49
|
+
output = [f"Found {len(results)} matches"]
|
|
50
|
+
|
|
51
|
+
current_file = ""
|
|
52
|
+
for result in results:
|
|
53
|
+
if current_file != result.file_path:
|
|
54
|
+
if current_file:
|
|
55
|
+
output.append("")
|
|
56
|
+
current_file = result.file_path
|
|
57
|
+
output.append(f"{result.file_path}:")
|
|
58
|
+
|
|
59
|
+
output.append(f" {result.line_number}: {result.line_content}")
|
|
60
|
+
|
|
61
|
+
return "\n".join(output)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _format_files_only(results: list[SearchResult], pattern: str) -> str:
|
|
65
|
+
"""Format results showing only file paths."""
|
|
66
|
+
files = sorted(set(r.file_path for r in results))
|
|
67
|
+
return "\n".join(files)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _format_count(results: list[SearchResult], pattern: str) -> str:
|
|
71
|
+
"""Format results showing match counts per file."""
|
|
72
|
+
file_counts: dict[str, int] = {}
|
|
73
|
+
for result in results:
|
|
74
|
+
file_counts[result.file_path] = file_counts.get(result.file_path, 0) + 1
|
|
75
|
+
|
|
76
|
+
sorted_counts = sorted(file_counts.items(), key=lambda x: (-x[1], x[0]))
|
|
77
|
+
return "\n".join(f"{count} {path}" for path, count in sorted_counts)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _format_json(results: list[SearchResult], pattern: str) -> str:
|
|
81
|
+
"""Format results as JSON."""
|
|
82
|
+
import json
|
|
83
|
+
|
|
84
|
+
json_results = [
|
|
85
|
+
{"file": r.file_path, "line": r.line_number, "content": r.line_content} for r in results
|
|
86
|
+
]
|
|
87
|
+
return json.dumps(json_results)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search result and configuration data structures for the grep tool.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SearchResult:
|
|
10
|
+
"""Represents a single search match with context."""
|
|
11
|
+
|
|
12
|
+
file_path: str
|
|
13
|
+
line_number: int
|
|
14
|
+
line_content: str
|
|
15
|
+
match_start: int
|
|
16
|
+
match_end: int
|
|
17
|
+
context_before: list[str]
|
|
18
|
+
context_after: list[str]
|
|
19
|
+
relevance_score: float = 0.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SearchConfig:
|
|
24
|
+
"""Configuration for search operations."""
|
|
25
|
+
|
|
26
|
+
case_sensitive: bool = False
|
|
27
|
+
use_regex: bool = False
|
|
28
|
+
max_results: int = 50
|
|
29
|
+
context_lines: int = 2
|
|
30
|
+
include_patterns: list[str] | None = None
|
|
31
|
+
exclude_patterns: list[str] | None = None
|
|
32
|
+
max_file_size: int = 1024 * 1024 # 1MB
|
|
33
|
+
timeout_seconds: int = 30
|
|
34
|
+
first_match_deadline: float = 3.0 # Timeout for finding first match
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Directory listing tool with recursive tree view for agent operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from tunacode.tools.decorators import base_tool
|
|
8
|
+
|
|
9
|
+
IGNORE_PATTERNS = [
|
|
10
|
+
"node_modules/",
|
|
11
|
+
"__pycache__/",
|
|
12
|
+
".git/",
|
|
13
|
+
"dist/",
|
|
14
|
+
"build/",
|
|
15
|
+
"target/",
|
|
16
|
+
"vendor/",
|
|
17
|
+
"bin/",
|
|
18
|
+
"obj/",
|
|
19
|
+
".idea/",
|
|
20
|
+
".vscode/",
|
|
21
|
+
".zig-cache/",
|
|
22
|
+
"zig-out/",
|
|
23
|
+
".coverage/",
|
|
24
|
+
"coverage/",
|
|
25
|
+
"tmp/",
|
|
26
|
+
"temp/",
|
|
27
|
+
".cache/",
|
|
28
|
+
"cache/",
|
|
29
|
+
"logs/",
|
|
30
|
+
".venv/",
|
|
31
|
+
"venv/",
|
|
32
|
+
"env/",
|
|
33
|
+
".ruff_cache/",
|
|
34
|
+
".pytest_cache/",
|
|
35
|
+
".mypy_cache/",
|
|
36
|
+
"*.egg-info/",
|
|
37
|
+
".eggs/",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
IGNORE_PATTERNS_COUNT = len(IGNORE_PATTERNS)
|
|
41
|
+
|
|
42
|
+
LIMIT = 100
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _should_ignore(path: str, ignore_patterns: list[str]) -> bool:
|
|
46
|
+
"""Check if path matches any ignore pattern."""
|
|
47
|
+
for pattern in ignore_patterns:
|
|
48
|
+
if pattern.endswith("/"):
|
|
49
|
+
# Directory pattern
|
|
50
|
+
dir_name = pattern.rstrip("/")
|
|
51
|
+
if f"/{dir_name}/" in f"/{path}/" or path.startswith(f"{dir_name}/"):
|
|
52
|
+
return True
|
|
53
|
+
elif "*" in pattern:
|
|
54
|
+
# Glob pattern (simple suffix match)
|
|
55
|
+
suffix = pattern.replace("*", "")
|
|
56
|
+
if path.endswith(suffix):
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _collect_files(
|
|
62
|
+
root: Path,
|
|
63
|
+
max_files: int,
|
|
64
|
+
show_hidden: bool,
|
|
65
|
+
ignore_patterns: list[str],
|
|
66
|
+
) -> list[str]:
|
|
67
|
+
"""Recursively collect files up to limit, respecting ignore patterns."""
|
|
68
|
+
files: list[str] = []
|
|
69
|
+
root_str = str(root)
|
|
70
|
+
|
|
71
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
72
|
+
rel_dir = os.path.relpath(dirpath, root_str)
|
|
73
|
+
if rel_dir == ".":
|
|
74
|
+
rel_dir = ""
|
|
75
|
+
|
|
76
|
+
# Filter directories in-place to prevent descending into ignored dirs
|
|
77
|
+
dirnames[:] = [
|
|
78
|
+
d
|
|
79
|
+
for d in dirnames
|
|
80
|
+
if (show_hidden or not d.startswith("."))
|
|
81
|
+
and not _should_ignore(f"{rel_dir}/{d}".lstrip("/") + "/", ignore_patterns)
|
|
82
|
+
]
|
|
83
|
+
dirnames.sort()
|
|
84
|
+
|
|
85
|
+
# Collect files
|
|
86
|
+
for filename in sorted(filenames):
|
|
87
|
+
if not show_hidden and filename.startswith("."):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
rel_path = f"{rel_dir}/{filename}".lstrip("/") if rel_dir else filename
|
|
91
|
+
if _should_ignore(rel_path, ignore_patterns):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
files.append(rel_path)
|
|
95
|
+
if len(files) >= max_files:
|
|
96
|
+
return files
|
|
97
|
+
|
|
98
|
+
return files
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
TREE_BRANCH = "├── "
|
|
102
|
+
TREE_LAST = "└── "
|
|
103
|
+
TREE_PIPE = "│ "
|
|
104
|
+
TREE_SPACE = " "
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _render_tree(base_name: str, files: list[str]) -> tuple[str, int, int]:
|
|
108
|
+
"""Build tree structure with connectors from file list.
|
|
109
|
+
|
|
110
|
+
Returns: (tree_output, file_count, dir_count)
|
|
111
|
+
"""
|
|
112
|
+
dirs: set[str] = set()
|
|
113
|
+
files_by_dir: dict[str, list[str]] = {}
|
|
114
|
+
|
|
115
|
+
for file in files:
|
|
116
|
+
dir_path = os.path.dirname(file)
|
|
117
|
+
parts = dir_path.split("/") if dir_path else []
|
|
118
|
+
|
|
119
|
+
for i in range(len(parts) + 1):
|
|
120
|
+
parent = "/".join(parts[:i]) if i > 0 else "."
|
|
121
|
+
dirs.add(parent)
|
|
122
|
+
|
|
123
|
+
key = dir_path if dir_path else "."
|
|
124
|
+
if key not in files_by_dir:
|
|
125
|
+
files_by_dir[key] = []
|
|
126
|
+
files_by_dir[key].append(os.path.basename(file))
|
|
127
|
+
|
|
128
|
+
file_count = len(files)
|
|
129
|
+
dir_count = len(dirs) - 1 # exclude root "."
|
|
130
|
+
|
|
131
|
+
def render_dir(dir_path: str, prefix: str) -> str:
|
|
132
|
+
output = ""
|
|
133
|
+
parent_match = "" if dir_path == "." else dir_path
|
|
134
|
+
child_dirs = sorted(d for d in dirs if d != dir_path and os.path.dirname(d) == parent_match)
|
|
135
|
+
child_files = sorted(files_by_dir.get(dir_path, []))
|
|
136
|
+
|
|
137
|
+
items: list[tuple[str, bool]] = [] # (name, is_dir)
|
|
138
|
+
for d in child_dirs:
|
|
139
|
+
items.append((os.path.basename(d), True))
|
|
140
|
+
for f in child_files:
|
|
141
|
+
items.append((f, False))
|
|
142
|
+
|
|
143
|
+
for i, (name, is_dir) in enumerate(items):
|
|
144
|
+
is_last = i == len(items) - 1
|
|
145
|
+
connector = TREE_LAST if is_last else TREE_BRANCH
|
|
146
|
+
child_prefix = prefix + (TREE_SPACE if is_last else TREE_PIPE)
|
|
147
|
+
|
|
148
|
+
if is_dir:
|
|
149
|
+
full_path = f"{parent_match}/{name}".lstrip("/") if parent_match else name
|
|
150
|
+
output += f"{prefix}{connector}{name}/\n"
|
|
151
|
+
output += render_dir(full_path, child_prefix)
|
|
152
|
+
else:
|
|
153
|
+
output += f"{prefix}{connector}{name}\n"
|
|
154
|
+
|
|
155
|
+
return output
|
|
156
|
+
|
|
157
|
+
return f"{base_name}/\n" + render_dir(".", ""), file_count, dir_count
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@base_tool
|
|
161
|
+
async def list_dir(
|
|
162
|
+
directory: str = ".",
|
|
163
|
+
max_files: int = LIMIT,
|
|
164
|
+
show_hidden: bool = False,
|
|
165
|
+
ignore: list[str] | None = None,
|
|
166
|
+
) -> str:
|
|
167
|
+
"""List directory contents as a recursive tree.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
directory: The path to the directory to list (defaults to current directory).
|
|
171
|
+
max_files: Maximum number of files to return (default: 100).
|
|
172
|
+
show_hidden: Whether to include hidden files/directories (default: False).
|
|
173
|
+
ignore: Additional glob patterns to ignore (default: None).
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Compact tree view of directory contents.
|
|
177
|
+
"""
|
|
178
|
+
dir_path = Path(directory).resolve()
|
|
179
|
+
|
|
180
|
+
if not dir_path.exists():
|
|
181
|
+
raise FileNotFoundError(f"Directory not found: {dir_path}")
|
|
182
|
+
|
|
183
|
+
if not dir_path.is_dir():
|
|
184
|
+
raise NotADirectoryError(f"Not a directory: {dir_path}")
|
|
185
|
+
|
|
186
|
+
# Combine default and custom ignore patterns
|
|
187
|
+
ignore_patterns = list(IGNORE_PATTERNS)
|
|
188
|
+
if ignore:
|
|
189
|
+
ignore_patterns.extend(ignore)
|
|
190
|
+
|
|
191
|
+
# Collect files in background thread
|
|
192
|
+
files = await asyncio.to_thread(
|
|
193
|
+
_collect_files, dir_path, max_files, show_hidden, ignore_patterns
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not files:
|
|
197
|
+
return f"{dir_path.name}/\n0 files 0 dirs"
|
|
198
|
+
|
|
199
|
+
tree_output, file_count, dir_count = _render_tree(dir_path.name, files)
|
|
200
|
+
summary = f"{file_count} files {dir_count} dirs"
|
|
201
|
+
|
|
202
|
+
if len(files) >= max_files:
|
|
203
|
+
summary += " (truncated)"
|
|
204
|
+
|
|
205
|
+
return f"{summary}\n{tree_output}"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Execute bash commands. Use for git, npm, docker, and system operations.
|
|
5
|
+
- Quote paths with spaces: cd "/path with spaces"
|
|
6
|
+
- Chain commands with && or ;
|
|
7
|
+
- Prefer Grep/Glob/Read tools over grep/find/cat commands
|
|
8
|
+
- Optional timeout in milliseconds (default 120000, max 600000)
|
|
9
|
+
</description>
|
|
10
|
+
</tool_prompt>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Fast content search using ripgrep. Supports regex patterns.
|
|
5
|
+
- Use output_mode "files_with_matches" for file paths only (most efficient)
|
|
6
|
+
- Use output_mode "content" for matching lines
|
|
7
|
+
- Use output_mode "count" for match counts per file
|
|
8
|
+
- Filter with glob parameter (e.g., "*.py")
|
|
9
|
+
</description>
|
|
10
|
+
</tool_prompt>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Clear the current todo list.
|
|
5
|
+
|
|
6
|
+
Use this tool when:
|
|
7
|
+
- Starting a new, unrelated task set
|
|
8
|
+
- All tasks are completed and you want a clean slate
|
|
9
|
+
|
|
10
|
+
Returns a confirmation message when the list is cleared.
|
|
11
|
+
</description>
|
|
12
|
+
</tool_prompt>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Read the current todo list to check task status and progress.
|
|
5
|
+
|
|
6
|
+
Use this tool to:
|
|
7
|
+
- Review current task state before starting work
|
|
8
|
+
- Check which tasks are pending, in progress, or completed
|
|
9
|
+
- Verify task completion status
|
|
10
|
+
|
|
11
|
+
Returns formatted list with status indicators:
|
|
12
|
+
- [ ] pending
|
|
13
|
+
- [>] in_progress
|
|
14
|
+
- [x] completed
|
|
15
|
+
</description>
|
|
16
|
+
</tool_prompt>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Create or update the task list for tracking progress during complex operations.
|
|
5
|
+
|
|
6
|
+
When to use:
|
|
7
|
+
- Complex multi-step tasks (3+ steps)
|
|
8
|
+
- User provides multiple tasks (numbered or comma-separated)
|
|
9
|
+
- After receiving new instructions to capture requirements
|
|
10
|
+
- When starting work on a task (mark as in_progress)
|
|
11
|
+
- After completing a task (mark as completed)
|
|
12
|
+
|
|
13
|
+
When NOT to use:
|
|
14
|
+
- Single, straightforward tasks
|
|
15
|
+
- Trivial tasks under 3 steps
|
|
16
|
+
- Purely conversational or informational requests
|
|
17
|
+
|
|
18
|
+
Schema:
|
|
19
|
+
- content: Task description in imperative form (e.g., "Fix the bug")
|
|
20
|
+
- status: "pending" | "in_progress" | "completed"
|
|
21
|
+
- activeForm: Present continuous form (e.g., "Fixing the bug")
|
|
22
|
+
|
|
23
|
+
Rules:
|
|
24
|
+
- Only ONE task should be in_progress at a time
|
|
25
|
+
- Mark tasks completed IMMEDIATELY after finishing
|
|
26
|
+
- Never mark incomplete work as completed
|
|
27
|
+
</description>
|
|
28
|
+
</tool_prompt>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Replace text in a file. Provide old_string and new_string.
|
|
5
|
+
- old_string must match exactly and be unique in the file
|
|
6
|
+
- Use replace_all=true to replace all occurrences
|
|
7
|
+
- Read the file first before editing
|
|
8
|
+
</description>
|
|
9
|
+
</tool_prompt>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<tool_prompt>
|
|
3
|
+
<description>
|
|
4
|
+
Fetch web content from a URL and return as readable text.
|
|
5
|
+
- Converts HTML pages to plain text for easier reading
|
|
6
|
+
- Blocks localhost and private IP addresses for security
|
|
7
|
+
- Maximum content size: 5MB
|
|
8
|
+
- Default timeout: 60 seconds
|
|
9
|
+
Use for fetching documentation, API references, or web resources.
|
|
10
|
+
</description>
|
|
11
|
+
</tool_prompt>
|