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.

Files changed (174) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/textual_repl.tcss +283 -0
  3. tunacode/configuration/__init__.py +1 -0
  4. tunacode/configuration/defaults.py +45 -0
  5. tunacode/configuration/models.py +147 -0
  6. tunacode/configuration/models_registry.json +1 -0
  7. tunacode/configuration/pricing.py +74 -0
  8. tunacode/configuration/settings.py +35 -0
  9. tunacode/constants.py +227 -0
  10. tunacode/core/__init__.py +6 -0
  11. tunacode/core/agents/__init__.py +39 -0
  12. tunacode/core/agents/agent_components/__init__.py +48 -0
  13. tunacode/core/agents/agent_components/agent_config.py +441 -0
  14. tunacode/core/agents/agent_components/agent_helpers.py +290 -0
  15. tunacode/core/agents/agent_components/message_handler.py +99 -0
  16. tunacode/core/agents/agent_components/node_processor.py +477 -0
  17. tunacode/core/agents/agent_components/response_state.py +129 -0
  18. tunacode/core/agents/agent_components/result_wrapper.py +51 -0
  19. tunacode/core/agents/agent_components/state_transition.py +112 -0
  20. tunacode/core/agents/agent_components/streaming.py +271 -0
  21. tunacode/core/agents/agent_components/task_completion.py +40 -0
  22. tunacode/core/agents/agent_components/tool_buffer.py +44 -0
  23. tunacode/core/agents/agent_components/tool_executor.py +101 -0
  24. tunacode/core/agents/agent_components/truncation_checker.py +37 -0
  25. tunacode/core/agents/delegation_tools.py +109 -0
  26. tunacode/core/agents/main.py +545 -0
  27. tunacode/core/agents/prompts.py +66 -0
  28. tunacode/core/agents/research_agent.py +231 -0
  29. tunacode/core/compaction.py +218 -0
  30. tunacode/core/prompting/__init__.py +27 -0
  31. tunacode/core/prompting/loader.py +66 -0
  32. tunacode/core/prompting/prompting_engine.py +98 -0
  33. tunacode/core/prompting/sections.py +50 -0
  34. tunacode/core/prompting/templates.py +69 -0
  35. tunacode/core/state.py +409 -0
  36. tunacode/exceptions.py +313 -0
  37. tunacode/indexing/__init__.py +5 -0
  38. tunacode/indexing/code_index.py +432 -0
  39. tunacode/indexing/constants.py +86 -0
  40. tunacode/lsp/__init__.py +112 -0
  41. tunacode/lsp/client.py +351 -0
  42. tunacode/lsp/diagnostics.py +19 -0
  43. tunacode/lsp/servers.py +101 -0
  44. tunacode/prompts/default_prompt.md +952 -0
  45. tunacode/prompts/research/sections/agent_role.xml +5 -0
  46. tunacode/prompts/research/sections/constraints.xml +14 -0
  47. tunacode/prompts/research/sections/output_format.xml +57 -0
  48. tunacode/prompts/research/sections/tool_use.xml +23 -0
  49. tunacode/prompts/sections/advanced_patterns.xml +255 -0
  50. tunacode/prompts/sections/agent_role.xml +8 -0
  51. tunacode/prompts/sections/completion.xml +10 -0
  52. tunacode/prompts/sections/critical_rules.xml +37 -0
  53. tunacode/prompts/sections/examples.xml +220 -0
  54. tunacode/prompts/sections/output_style.xml +94 -0
  55. tunacode/prompts/sections/parallel_exec.xml +105 -0
  56. tunacode/prompts/sections/search_pattern.xml +100 -0
  57. tunacode/prompts/sections/system_info.xml +6 -0
  58. tunacode/prompts/sections/tool_use.xml +84 -0
  59. tunacode/prompts/sections/user_instructions.xml +3 -0
  60. tunacode/py.typed +0 -0
  61. tunacode/templates/__init__.py +5 -0
  62. tunacode/templates/loader.py +15 -0
  63. tunacode/tools/__init__.py +10 -0
  64. tunacode/tools/authorization/__init__.py +29 -0
  65. tunacode/tools/authorization/context.py +32 -0
  66. tunacode/tools/authorization/factory.py +20 -0
  67. tunacode/tools/authorization/handler.py +58 -0
  68. tunacode/tools/authorization/notifier.py +35 -0
  69. tunacode/tools/authorization/policy.py +19 -0
  70. tunacode/tools/authorization/requests.py +119 -0
  71. tunacode/tools/authorization/rules.py +72 -0
  72. tunacode/tools/bash.py +222 -0
  73. tunacode/tools/decorators.py +213 -0
  74. tunacode/tools/glob.py +353 -0
  75. tunacode/tools/grep.py +468 -0
  76. tunacode/tools/grep_components/__init__.py +9 -0
  77. tunacode/tools/grep_components/file_filter.py +93 -0
  78. tunacode/tools/grep_components/pattern_matcher.py +158 -0
  79. tunacode/tools/grep_components/result_formatter.py +87 -0
  80. tunacode/tools/grep_components/search_result.py +34 -0
  81. tunacode/tools/list_dir.py +205 -0
  82. tunacode/tools/prompts/bash_prompt.xml +10 -0
  83. tunacode/tools/prompts/glob_prompt.xml +7 -0
  84. tunacode/tools/prompts/grep_prompt.xml +10 -0
  85. tunacode/tools/prompts/list_dir_prompt.xml +7 -0
  86. tunacode/tools/prompts/read_file_prompt.xml +9 -0
  87. tunacode/tools/prompts/todoclear_prompt.xml +12 -0
  88. tunacode/tools/prompts/todoread_prompt.xml +16 -0
  89. tunacode/tools/prompts/todowrite_prompt.xml +28 -0
  90. tunacode/tools/prompts/update_file_prompt.xml +9 -0
  91. tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
  92. tunacode/tools/prompts/write_file_prompt.xml +7 -0
  93. tunacode/tools/react.py +111 -0
  94. tunacode/tools/read_file.py +68 -0
  95. tunacode/tools/todo.py +222 -0
  96. tunacode/tools/update_file.py +62 -0
  97. tunacode/tools/utils/__init__.py +1 -0
  98. tunacode/tools/utils/ripgrep.py +311 -0
  99. tunacode/tools/utils/text_match.py +352 -0
  100. tunacode/tools/web_fetch.py +245 -0
  101. tunacode/tools/write_file.py +34 -0
  102. tunacode/tools/xml_helper.py +34 -0
  103. tunacode/types/__init__.py +166 -0
  104. tunacode/types/base.py +94 -0
  105. tunacode/types/callbacks.py +53 -0
  106. tunacode/types/dataclasses.py +121 -0
  107. tunacode/types/pydantic_ai.py +31 -0
  108. tunacode/types/state.py +122 -0
  109. tunacode/ui/__init__.py +6 -0
  110. tunacode/ui/app.py +542 -0
  111. tunacode/ui/commands/__init__.py +430 -0
  112. tunacode/ui/components/__init__.py +1 -0
  113. tunacode/ui/headless/__init__.py +5 -0
  114. tunacode/ui/headless/output.py +72 -0
  115. tunacode/ui/main.py +252 -0
  116. tunacode/ui/renderers/__init__.py +41 -0
  117. tunacode/ui/renderers/errors.py +197 -0
  118. tunacode/ui/renderers/panels.py +550 -0
  119. tunacode/ui/renderers/search.py +314 -0
  120. tunacode/ui/renderers/tools/__init__.py +21 -0
  121. tunacode/ui/renderers/tools/bash.py +247 -0
  122. tunacode/ui/renderers/tools/diagnostics.py +186 -0
  123. tunacode/ui/renderers/tools/glob.py +226 -0
  124. tunacode/ui/renderers/tools/grep.py +228 -0
  125. tunacode/ui/renderers/tools/list_dir.py +198 -0
  126. tunacode/ui/renderers/tools/read_file.py +226 -0
  127. tunacode/ui/renderers/tools/research.py +294 -0
  128. tunacode/ui/renderers/tools/update_file.py +237 -0
  129. tunacode/ui/renderers/tools/web_fetch.py +182 -0
  130. tunacode/ui/repl_support.py +226 -0
  131. tunacode/ui/screens/__init__.py +16 -0
  132. tunacode/ui/screens/model_picker.py +303 -0
  133. tunacode/ui/screens/session_picker.py +181 -0
  134. tunacode/ui/screens/setup.py +218 -0
  135. tunacode/ui/screens/theme_picker.py +90 -0
  136. tunacode/ui/screens/update_confirm.py +69 -0
  137. tunacode/ui/shell_runner.py +129 -0
  138. tunacode/ui/styles/layout.tcss +98 -0
  139. tunacode/ui/styles/modals.tcss +38 -0
  140. tunacode/ui/styles/panels.tcss +81 -0
  141. tunacode/ui/styles/theme-nextstep.tcss +303 -0
  142. tunacode/ui/styles/widgets.tcss +33 -0
  143. tunacode/ui/styles.py +18 -0
  144. tunacode/ui/widgets/__init__.py +23 -0
  145. tunacode/ui/widgets/command_autocomplete.py +62 -0
  146. tunacode/ui/widgets/editor.py +402 -0
  147. tunacode/ui/widgets/file_autocomplete.py +47 -0
  148. tunacode/ui/widgets/messages.py +46 -0
  149. tunacode/ui/widgets/resource_bar.py +182 -0
  150. tunacode/ui/widgets/status_bar.py +98 -0
  151. tunacode/utils/__init__.py +0 -0
  152. tunacode/utils/config/__init__.py +13 -0
  153. tunacode/utils/config/user_configuration.py +91 -0
  154. tunacode/utils/messaging/__init__.py +10 -0
  155. tunacode/utils/messaging/message_utils.py +34 -0
  156. tunacode/utils/messaging/token_counter.py +77 -0
  157. tunacode/utils/parsing/__init__.py +13 -0
  158. tunacode/utils/parsing/command_parser.py +55 -0
  159. tunacode/utils/parsing/json_utils.py +188 -0
  160. tunacode/utils/parsing/retry.py +146 -0
  161. tunacode/utils/parsing/tool_parser.py +267 -0
  162. tunacode/utils/security/__init__.py +15 -0
  163. tunacode/utils/security/command.py +106 -0
  164. tunacode/utils/system/__init__.py +25 -0
  165. tunacode/utils/system/gitignore.py +155 -0
  166. tunacode/utils/system/paths.py +190 -0
  167. tunacode/utils/ui/__init__.py +9 -0
  168. tunacode/utils/ui/file_filter.py +135 -0
  169. tunacode/utils/ui/helpers.py +24 -0
  170. tunacode_cli-0.1.21.dist-info/METADATA +170 -0
  171. tunacode_cli-0.1.21.dist-info/RECORD +174 -0
  172. tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
  173. tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
  174. 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 &amp;&amp; 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,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <tool_prompt>
3
+ <description>
4
+ Fast file pattern matching. Supports glob patterns like "**/*.py" or "src/**/*.ts".
5
+ Returns matching file paths sorted by modification time.
6
+ </description>
7
+ </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,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <tool_prompt>
3
+ <description>
4
+ List files and directories. Requires absolute path.
5
+ Prefer Glob/Grep when you know what to search for.
6
+ </description>
7
+ </tool_prompt>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <tool_prompt>
3
+ <description>
4
+ Read file contents. Requires absolute filepath.
5
+ - Reads up to 2000 lines by default
6
+ - Use offset/limit for large files
7
+ - Can read images and PDFs
8
+ </description>
9
+ </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>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <tool_prompt>
3
+ <description>
4
+ Write content to a file. Creates or overwrites.
5
+ Requires absolute filepath. Read existing files first before overwriting.
6
+ </description>
7
+ </tool_prompt>