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
tunacode/tools/glob.py ADDED
@@ -0,0 +1,353 @@
1
+ """Glob tool for fast file pattern matching."""
2
+
3
+ import asyncio
4
+ import fnmatch
5
+ import os
6
+ import re
7
+ from enum import Enum
8
+ from pathlib import Path
9
+
10
+ from tunacode.indexing import CodeIndex
11
+ from tunacode.tools.decorators import base_tool
12
+
13
+ MAX_RESULTS = 5000
14
+ EXCLUDE_DIRS = {
15
+ "node_modules",
16
+ ".git",
17
+ "__pycache__",
18
+ ".venv",
19
+ "venv",
20
+ "dist",
21
+ "build",
22
+ ".pytest_cache",
23
+ ".mypy_cache",
24
+ ".tox",
25
+ "target",
26
+ ".next",
27
+ ".nuxt",
28
+ "coverage",
29
+ ".coverage",
30
+ }
31
+
32
+
33
+ class SortOrder(Enum):
34
+ """Sorting options for glob results."""
35
+
36
+ MODIFIED = "modified"
37
+ SIZE = "size"
38
+ ALPHABETICAL = "alphabetical"
39
+ DEPTH = "depth"
40
+
41
+
42
+ # Module-level cache for gitignore patterns
43
+ _gitignore_patterns: set[str] | None = None
44
+
45
+
46
+ @base_tool
47
+ async def glob(
48
+ pattern: str,
49
+ directory: str = ".",
50
+ recursive: bool = True,
51
+ include_hidden: bool = False,
52
+ exclude_dirs: list[str] | None = None,
53
+ max_results: int = MAX_RESULTS,
54
+ sort_by: str = "modified",
55
+ case_sensitive: bool = False,
56
+ use_gitignore: bool = True,
57
+ ) -> str:
58
+ """Find files matching glob patterns.
59
+
60
+ Args:
61
+ pattern: Glob pattern to match (e.g., "*.py", "**/*.{js,ts}").
62
+ directory: Directory to search in (default: current directory).
63
+ recursive: Whether to search recursively (default: True).
64
+ include_hidden: Whether to include hidden files/directories.
65
+ exclude_dirs: Additional directories to exclude from search.
66
+ max_results: Maximum number of results to return.
67
+ sort_by: How to sort results (modified/size/alphabetical/depth).
68
+ case_sensitive: Whether pattern matching is case-sensitive.
69
+ use_gitignore: Whether to respect .gitignore patterns.
70
+
71
+ Returns:
72
+ Formatted list of matching file paths.
73
+ """
74
+ root_path = Path(directory).resolve()
75
+ if not root_path.exists():
76
+ return f"Error: Directory '{directory}' does not exist"
77
+ if not root_path.is_dir():
78
+ return f"Error: '{directory}' is not a directory"
79
+
80
+ all_exclude = EXCLUDE_DIRS.copy()
81
+ if exclude_dirs:
82
+ all_exclude.update(exclude_dirs)
83
+
84
+ sort_order = _parse_sort_order(sort_by)
85
+ patterns = _expand_brace_pattern(pattern)
86
+
87
+ if use_gitignore:
88
+ await _load_gitignore_patterns(root_path)
89
+
90
+ # Try CodeIndex for faster lookup
91
+ code_index = _get_code_index(directory)
92
+ source = "filesystem"
93
+
94
+ if code_index and not include_hidden and recursive:
95
+ matches = await _glob_with_index(
96
+ code_index, patterns, root_path, all_exclude, max_results, case_sensitive
97
+ )
98
+ source = "index"
99
+ else:
100
+ matches = await _glob_filesystem(
101
+ root_path, patterns, recursive, include_hidden, all_exclude, max_results, case_sensitive
102
+ )
103
+
104
+ if not matches:
105
+ return f"[source:{source}]\nNo files found matching pattern: {pattern}"
106
+
107
+ matches = await _sort_matches(matches, sort_order)
108
+ return _format_output(pattern, matches, max_results, source)
109
+
110
+
111
+ def _parse_sort_order(sort_by: str) -> SortOrder:
112
+ """Parse sort order string to enum."""
113
+ try:
114
+ return SortOrder(sort_by)
115
+ except ValueError:
116
+ return SortOrder.MODIFIED
117
+
118
+
119
+ def _get_code_index(directory: str) -> CodeIndex | None:
120
+ """Get CodeIndex instance if searching from project root."""
121
+ if directory != "." and directory != os.getcwd():
122
+ return None
123
+ try:
124
+ index = CodeIndex.get_instance()
125
+ index.build_index()
126
+ return index
127
+ except Exception:
128
+ return None
129
+
130
+
131
+ def _expand_brace_pattern(pattern: str) -> list[str]:
132
+ """Expand brace patterns like "*.{py,js,ts}" into multiple patterns."""
133
+ if "{" not in pattern or "}" not in pattern:
134
+ return [pattern]
135
+
136
+ expanded = []
137
+ stack = [pattern]
138
+
139
+ while stack:
140
+ current = stack.pop()
141
+ start = -1
142
+ depth = 0
143
+
144
+ for i, char in enumerate(current):
145
+ if char == "{":
146
+ if depth == 0:
147
+ start = i
148
+ depth += 1
149
+ elif char == "}":
150
+ depth -= 1
151
+ if depth == 0 and start != -1:
152
+ prefix = current[:start]
153
+ suffix = current[i + 1 :]
154
+ options = current[start + 1 : i].split(",")
155
+
156
+ for option in options:
157
+ new_pattern = prefix + option.strip() + suffix
158
+ if "{" in new_pattern:
159
+ stack.append(new_pattern)
160
+ else:
161
+ expanded.append(new_pattern)
162
+ break
163
+ else:
164
+ expanded.append(current)
165
+
166
+ return expanded
167
+
168
+
169
+ async def _load_gitignore_patterns(root: Path) -> None:
170
+ """Load .gitignore patterns from the repository."""
171
+ global _gitignore_patterns
172
+ if _gitignore_patterns is not None:
173
+ return
174
+
175
+ _gitignore_patterns = set()
176
+ ignore_files = [".gitignore", ".ignore", ".rgignore"]
177
+
178
+ for ignore_file in ignore_files:
179
+ ignore_path = root / ignore_file
180
+ if ignore_path.exists():
181
+ try:
182
+ with open(ignore_path, encoding="utf-8") as f:
183
+ for line in f:
184
+ line = line.strip()
185
+ if line and not line.startswith("#"):
186
+ _gitignore_patterns.add(line)
187
+ except Exception:
188
+ pass
189
+
190
+
191
+ async def _glob_with_index(
192
+ code_index: CodeIndex,
193
+ patterns: list[str],
194
+ root: Path,
195
+ exclude_dirs: set,
196
+ max_results: int,
197
+ case_sensitive: bool,
198
+ ) -> list[str]:
199
+ """Use CodeIndex for faster file matching."""
200
+ all_files = code_index.get_all_files()
201
+ matches = []
202
+
203
+ for file_path in all_files:
204
+ abs_path = code_index.root_dir / file_path
205
+
206
+ for pattern in patterns:
207
+ if _match_pattern(str(file_path), pattern, case_sensitive):
208
+ if not any(d in file_path.parts for d in exclude_dirs):
209
+ matches.append(str(abs_path))
210
+ if len(matches) >= max_results:
211
+ return matches
212
+ break
213
+
214
+ return matches
215
+
216
+
217
+ def _match_pattern(path: str, pattern: str, case_sensitive: bool) -> bool:
218
+ """Match a path against a glob pattern."""
219
+ if "**" in pattern:
220
+ if pattern.startswith("**/"):
221
+ suffix = pattern[3:]
222
+ if case_sensitive:
223
+ if fnmatch.fnmatch(path, suffix):
224
+ return True
225
+ else:
226
+ if fnmatch.fnmatch(path.lower(), suffix.lower()):
227
+ return True
228
+
229
+ regex_pat = pattern.replace("**", "__STARSTAR__")
230
+ regex_pat = fnmatch.translate(regex_pat)
231
+ regex_pat = regex_pat.replace("__STARSTAR__", ".*")
232
+ flags = 0 if case_sensitive else re.IGNORECASE
233
+ return bool(re.match(regex_pat, path, flags))
234
+ else:
235
+ if case_sensitive:
236
+ return fnmatch.fnmatch(path, pattern)
237
+ return fnmatch.fnmatch(path.lower(), pattern.lower())
238
+
239
+
240
+ async def _glob_filesystem(
241
+ root: Path,
242
+ patterns: list[str],
243
+ recursive: bool,
244
+ include_hidden: bool,
245
+ exclude_dirs: set,
246
+ max_results: int,
247
+ case_sensitive: bool,
248
+ ) -> list[str]:
249
+ """Perform glob search using os.scandir."""
250
+
251
+ def search_sync():
252
+ matches = []
253
+ stack = [root]
254
+ flags = 0 if case_sensitive else re.IGNORECASE
255
+
256
+ compiled = []
257
+ for pat in patterns:
258
+ if "**" in pat:
259
+ regex_pat = pat.replace("**", "__STARSTAR__")
260
+ regex_pat = fnmatch.translate(regex_pat)
261
+ regex_pat = regex_pat.replace("__STARSTAR__", ".*")
262
+ compiled.append((pat, re.compile(regex_pat, flags)))
263
+ else:
264
+ compiled.append((pat, re.compile(fnmatch.translate(pat), flags)))
265
+
266
+ while stack and len(matches) < max_results:
267
+ current = stack.pop()
268
+
269
+ try:
270
+ with os.scandir(current) as entries:
271
+ for entry in entries:
272
+ if not include_hidden and entry.name.startswith("."):
273
+ continue
274
+
275
+ if entry.is_dir(follow_symlinks=False):
276
+ if entry.name not in exclude_dirs and recursive:
277
+ stack.append(Path(entry.path))
278
+ elif entry.is_file(follow_symlinks=False):
279
+ rel_path = os.path.relpath(entry.path, root)
280
+
281
+ for orig, comp in compiled:
282
+ if "**" in orig:
283
+ if orig.startswith("**/") and not recursive:
284
+ suffix = orig[3:]
285
+ if fnmatch.fnmatch(entry.name, suffix):
286
+ matches.append(entry.path)
287
+ break
288
+ elif comp.match(rel_path):
289
+ matches.append(entry.path)
290
+ break
291
+ elif orig.startswith("**/"):
292
+ suffix = orig[3:]
293
+ if fnmatch.fnmatch(entry.name, suffix):
294
+ matches.append(entry.path)
295
+ break
296
+ else:
297
+ if comp.match(entry.name):
298
+ matches.append(entry.path)
299
+ break
300
+
301
+ if len(matches) >= max_results:
302
+ break
303
+
304
+ except (PermissionError, OSError):
305
+ continue
306
+
307
+ return matches[:max_results]
308
+
309
+ return await asyncio.get_event_loop().run_in_executor(None, search_sync)
310
+
311
+
312
+ async def _sort_matches(matches: list[str], sort_by: SortOrder) -> list[str]:
313
+ """Sort matches based on the specified order."""
314
+ if not matches:
315
+ return matches
316
+
317
+ def sort_sync():
318
+ if sort_by == SortOrder.MODIFIED:
319
+ return sorted(matches, key=lambda p: os.path.getmtime(p), reverse=True)
320
+ elif sort_by == SortOrder.SIZE:
321
+ return sorted(matches, key=lambda p: os.path.getsize(p), reverse=True)
322
+ elif sort_by == SortOrder.DEPTH:
323
+ return sorted(matches, key=lambda p: (p.count(os.sep), p))
324
+ return sorted(matches)
325
+
326
+ return await asyncio.get_event_loop().run_in_executor(None, sort_sync)
327
+
328
+
329
+ def _format_output(pattern: str, matches: list[str], max_results: int, source: str) -> str:
330
+ """Format glob results with source marker and header for rich panel parsing.
331
+
332
+ Args:
333
+ pattern: The glob pattern used.
334
+ matches: List of matching file paths.
335
+ max_results: Maximum results limit.
336
+ source: "index" or "filesystem" to indicate cache hit/miss.
337
+
338
+ Returns:
339
+ Formatted output with source marker and file count header.
340
+ """
341
+ parts = [f"[source:{source}]"]
342
+ file_count = len(matches)
343
+ file_word = "file" if file_count == 1 else "files"
344
+ parts.append(f"Found {file_count} {file_word} matching pattern: {pattern}")
345
+
346
+ if matches:
347
+ parts.append("") # Blank line
348
+ parts.extend(matches)
349
+
350
+ if file_count == max_results:
351
+ parts.append(f"(truncated at {max_results})")
352
+
353
+ return "\n".join(parts)