siada-cli 0.0.1__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.
Files changed (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,326 @@
1
+ """
2
+ File filtering utilities for ReadManyFiles tool.
3
+ """
4
+
5
+ import os
6
+ import fnmatch
7
+ from pathlib import Path
8
+ from typing import List, Set, Dict, Tuple, Optional
9
+
10
+ from .models import DEFAULT_EXCLUDES
11
+
12
+
13
+ class FileFilter:
14
+ """File filtering utility class"""
15
+
16
+ def __init__(self, target_dir: str):
17
+ self.target_dir = Path(target_dir).resolve()
18
+ self._gitignore_cache: Dict[str, List[str]] = {}
19
+
20
+ def should_exclude_file(self, file_path: str, workspace_dir: str,
21
+ exclusion_patterns: List[str]) -> bool:
22
+ """
23
+ Check if file should be excluded based on exclusion patterns
24
+
25
+ Args:
26
+ file_path: Absolute path to the file
27
+ workspace_dir: Workspace directory path
28
+ exclusion_patterns: List of glob exclusion patterns
29
+
30
+ Returns:
31
+ True if file should be excluded, False otherwise
32
+ """
33
+ try:
34
+ relative_path = os.path.relpath(file_path, workspace_dir)
35
+ normalized_path = relative_path.replace('\\', '/')
36
+ except ValueError:
37
+ # Handle case where paths are on different drives (Windows)
38
+ return True
39
+
40
+ for pattern in exclusion_patterns:
41
+ # Normalize pattern
42
+ normalized_pattern = pattern.replace('\\', '/')
43
+
44
+ # Direct file name matching
45
+ if fnmatch.fnmatch(normalized_path, normalized_pattern):
46
+ return True
47
+
48
+ # Directory level matching
49
+ if pattern.endswith('/**'):
50
+ dir_pattern = pattern[:-3]
51
+ if (normalized_path.startswith(dir_pattern + '/') or
52
+ normalized_path == dir_pattern):
53
+ return True
54
+
55
+ # File extension matching
56
+ if pattern.startswith('**/*.'):
57
+ ext_pattern = pattern[3:] # Remove '**/'
58
+ if fnmatch.fnmatch(os.path.basename(normalized_path), ext_pattern):
59
+ return True
60
+
61
+ return False
62
+
63
+ def build_exclusion_patterns(self, params) -> List[str]:
64
+ """
65
+ Build complete list of exclusion patterns
66
+
67
+ Args:
68
+ params: ReadManyFilesParams object
69
+
70
+ Returns:
71
+ List of exclusion patterns
72
+ """
73
+ exclusion_patterns = []
74
+
75
+ # Add default excludes if enabled
76
+ if params.useDefaultExcludes:
77
+ exclusion_patterns.extend(DEFAULT_EXCLUDES)
78
+
79
+ # Add user-specified excludes
80
+ if params.exclude:
81
+ exclusion_patterns.extend(params.exclude)
82
+
83
+ return exclusion_patterns
84
+
85
+ def apply_gitignore_filters(self, file_paths: Set[str],
86
+ filtering_options: Dict[str, bool]) -> Tuple[List[str], Dict[str, int]]:
87
+ """
88
+ Apply .gitignore filtering rules
89
+
90
+ Args:
91
+ file_paths: Set of absolute file paths
92
+ filtering_options: Filtering configuration options
93
+
94
+ Returns:
95
+ Tuple of (filtered_files, filter_counts)
96
+ """
97
+ # Convert to relative paths for filtering
98
+ relative_paths = []
99
+ for abs_path in file_paths:
100
+ try:
101
+ rel_path = os.path.relpath(abs_path, self.target_dir)
102
+ relative_paths.append(rel_path)
103
+ except ValueError:
104
+ # Skip files outside target directory
105
+ continue
106
+
107
+ filter_counts = {'git_ignored': 0}
108
+
109
+ # Apply git ignore filtering if enabled
110
+ if filtering_options.get('respect_git_ignore', True):
111
+ git_filtered = self._filter_with_gitignore(relative_paths)
112
+ filter_counts['git_ignored'] = len(relative_paths) - len(git_filtered)
113
+ relative_paths = git_filtered
114
+
115
+ # Convert back to absolute paths
116
+ final_files = [
117
+ os.path.join(self.target_dir, rel_path)
118
+ for rel_path in relative_paths
119
+ ]
120
+
121
+ return final_files, filter_counts
122
+
123
+ def _filter_with_gitignore(self, relative_paths: List[str]) -> List[str]:
124
+ """
125
+ Filter files using .gitignore rules
126
+
127
+ Args:
128
+ relative_paths: List of relative file paths
129
+
130
+ Returns:
131
+ List of filtered relative paths
132
+ """
133
+ # Find all .gitignore files in the directory tree
134
+ gitignore_files = self._find_gitignore_files()
135
+
136
+ if not gitignore_files:
137
+ return relative_paths
138
+
139
+ # Parse all .gitignore files
140
+ ignore_patterns = []
141
+ for gitignore_file in gitignore_files:
142
+ patterns = self._parse_gitignore_file(gitignore_file)
143
+ ignore_patterns.extend(patterns)
144
+
145
+ # Filter files based on patterns
146
+ filtered_paths = []
147
+ for path in relative_paths:
148
+ if not self._is_ignored_by_gitignore(path, ignore_patterns):
149
+ filtered_paths.append(path)
150
+
151
+ return filtered_paths
152
+
153
+ def _find_gitignore_files(self) -> List[str]:
154
+ """
155
+ Find all .gitignore files in the target directory tree (excluding hidden directories)
156
+
157
+ Returns:
158
+ List of .gitignore file paths
159
+ """
160
+ gitignore_files = []
161
+
162
+ for root, dirs, files in os.walk(self.target_dir):
163
+ # Filter out hidden directories from further traversal
164
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
165
+
166
+ if '.gitignore' in files:
167
+ gitignore_path = os.path.join(root, '.gitignore')
168
+ gitignore_files.append(gitignore_path)
169
+
170
+ return gitignore_files
171
+
172
+ def _parse_gitignore_file(self, gitignore_path: str) -> List[Tuple[str, str]]:
173
+ """
174
+ Parse .gitignore file and return patterns
175
+
176
+ Args:
177
+ gitignore_path: Path to .gitignore file
178
+
179
+ Returns:
180
+ List of (pattern_type, pattern) tuples
181
+ """
182
+ if gitignore_path in self._gitignore_cache:
183
+ return self._gitignore_cache[gitignore_path]
184
+
185
+ patterns = []
186
+
187
+ try:
188
+ with open(gitignore_path, 'r', encoding='utf-8') as f:
189
+ for line in f:
190
+ line = line.strip()
191
+
192
+ # Skip empty lines and comments
193
+ if not line or line.startswith('#'):
194
+ continue
195
+
196
+ # Handle negation patterns (starting with !)
197
+ if line.startswith('!'):
198
+ patterns.append(('negate', line[1:]))
199
+ else:
200
+ patterns.append(('ignore', line))
201
+
202
+ except (FileNotFoundError, UnicodeDecodeError, PermissionError):
203
+ # Ignore errors reading .gitignore files
204
+ pass
205
+
206
+ self._gitignore_cache[gitignore_path] = patterns
207
+ return patterns
208
+
209
+ def _is_ignored_by_gitignore(self, file_path: str,
210
+ patterns: List[Tuple[str, str]]) -> bool:
211
+ """
212
+ Check if file is ignored by gitignore patterns
213
+
214
+ Args:
215
+ file_path: Relative file path
216
+ patterns: List of (pattern_type, pattern) tuples
217
+
218
+ Returns:
219
+ True if file should be ignored, False otherwise
220
+ """
221
+ normalized_path = file_path.replace('\\', '/')
222
+ is_ignored = False
223
+
224
+ for pattern_type, pattern in patterns:
225
+ # Normalize pattern
226
+ normalized_pattern = pattern.replace('\\', '/')
227
+
228
+ # Check if pattern matches
229
+ if self._gitignore_pattern_matches(normalized_path, normalized_pattern):
230
+ if pattern_type == 'ignore':
231
+ is_ignored = True
232
+ elif pattern_type == 'negate':
233
+ is_ignored = False
234
+
235
+ return is_ignored
236
+
237
+ def _gitignore_pattern_matches(self, file_path: str, pattern: str) -> bool:
238
+ """
239
+ Check if gitignore pattern matches file path
240
+
241
+ Args:
242
+ file_path: Normalized file path
243
+ pattern: Normalized gitignore pattern
244
+
245
+ Returns:
246
+ True if pattern matches, False otherwise
247
+ """
248
+ # Handle directory patterns (ending with /)
249
+ if pattern.endswith('/'):
250
+ # This is a directory pattern, check if file is in this directory
251
+ dir_pattern = pattern[:-1]
252
+ return file_path.startswith(dir_pattern + '/') or file_path == dir_pattern
253
+
254
+ # Handle patterns starting with /
255
+ if pattern.startswith('/'):
256
+ # Absolute pattern from repository root
257
+ pattern = pattern[1:]
258
+ return fnmatch.fnmatch(file_path, pattern)
259
+
260
+ # Handle patterns with **
261
+ if '**' in pattern:
262
+ return fnmatch.fnmatch(file_path, pattern)
263
+
264
+ # Handle simple patterns
265
+ # Check if pattern matches the file name or any parent directory
266
+ path_parts = file_path.split('/')
267
+
268
+ # Check against full path
269
+ if fnmatch.fnmatch(file_path, pattern):
270
+ return True
271
+
272
+ # Check against file name only
273
+ if fnmatch.fnmatch(path_parts[-1], pattern):
274
+ return True
275
+
276
+ # Check against any directory in the path
277
+ for part in path_parts[:-1]: # Exclude the file name
278
+ if fnmatch.fnmatch(part, pattern):
279
+ return True
280
+
281
+ return False
282
+
283
+ def validate_workspace_security(self, file_paths: List[str]) -> List[str]:
284
+ """
285
+ Validate that all file paths are within the workspace directory
286
+
287
+ Args:
288
+ file_paths: List of absolute file paths
289
+
290
+ Returns:
291
+ List of validated file paths (within workspace)
292
+ """
293
+ validated_files = []
294
+
295
+ for file_path in file_paths:
296
+ try:
297
+ # Resolve the file path to handle symlinks and relative paths
298
+ resolved_path = Path(file_path).resolve()
299
+
300
+ # Check if the resolved path is within the target directory
301
+ if self._is_path_within_target(resolved_path):
302
+ validated_files.append(str(resolved_path))
303
+
304
+ except (OSError, ValueError):
305
+ # Skip files that can't be resolved or are invalid
306
+ continue
307
+
308
+ return validated_files
309
+
310
+ def _is_path_within_target(self, file_path: Path) -> bool:
311
+ """
312
+ Check if file path is within the target directory
313
+
314
+ Args:
315
+ file_path: Resolved file path
316
+
317
+ Returns:
318
+ True if path is within target directory, False otherwise
319
+ """
320
+ try:
321
+ # Check if file_path is relative to target_dir
322
+ file_path.relative_to(self.target_dir)
323
+ return True
324
+ except ValueError:
325
+ # Path is not within target directory
326
+ return False
@@ -0,0 +1,204 @@
1
+ """
2
+ Output formatting utilities for ReadManyFiles tool.
3
+ """
4
+
5
+ from typing import List, Dict, Any
6
+ from .models import ProcessingStats
7
+
8
+
9
+ class ResultFormatter:
10
+ """Result formatting utility class"""
11
+
12
+ def __init__(self, target_dir: str):
13
+ self.target_dir = target_dir
14
+
15
+ def build_result(self, content_parts: List[Any],
16
+ processed_files: List[str],
17
+ skipped_files: List[Dict],
18
+ stats: ProcessingStats) -> Dict[str, Any]:
19
+ """
20
+ Build final tool result
21
+
22
+ Args:
23
+ content_parts: List of processed file contents
24
+ processed_files: List of successfully processed file paths
25
+ skipped_files: List of skipped file information
26
+ stats: Processing statistics
27
+
28
+ Returns:
29
+ Dictionary with llmContent and returnDisplay
30
+ """
31
+ # Build LLM content
32
+ if not content_parts:
33
+ llm_content = ['No files matching the criteria were found or all were skipped.']
34
+ else:
35
+ llm_content = content_parts
36
+
37
+ # Build display message
38
+ display_message = self.build_display_message(
39
+ processed_files, skipped_files, stats
40
+ )
41
+
42
+ return {
43
+ 'llmContent': llm_content,
44
+ 'returnDisplay': display_message.strip()
45
+ }
46
+
47
+ def build_display_message(self, processed_files: List[str],
48
+ skipped_files: List[Dict],
49
+ stats: ProcessingStats) -> str:
50
+ """
51
+ Build detailed user interface display message
52
+
53
+ Args:
54
+ processed_files: List of successfully processed file paths
55
+ skipped_files: List of skipped file information
56
+ stats: Processing statistics
57
+
58
+ Returns:
59
+ Formatted display message string
60
+ """
61
+ lines = [f"### ReadManyFiles Result (Target Dir: `{self.target_dir}`)\n"]
62
+
63
+ # Success section
64
+ if processed_files:
65
+ count = len(processed_files)
66
+ lines.append(f"Successfully read and concatenated content from **{count} file(s)**.\n")
67
+
68
+ # File type breakdown
69
+ type_info = []
70
+ if stats.text_files > 0:
71
+ type_info.append(f"{stats.text_files} text")
72
+ if stats.image_files > 0:
73
+ type_info.append(f"{stats.image_files} image")
74
+ if stats.pdf_files > 0:
75
+ type_info.append(f"{stats.pdf_files} PDF")
76
+ if stats.binary_files > 0:
77
+ type_info.append(f"{stats.binary_files} binary")
78
+
79
+ if type_info:
80
+ lines.append(f"**File Types:** {', '.join(type_info)} files\n")
81
+
82
+ # Size information
83
+ if stats.total_size > 0:
84
+ size_mb = stats.total_size / (1024 * 1024)
85
+ if size_mb >= 1:
86
+ lines.append(f"**Total Size:** {size_mb:.1f} MB\n")
87
+ else:
88
+ size_kb = stats.total_size / 1024
89
+ lines.append(f"**Total Size:** {size_kb:.1f} KB\n")
90
+
91
+ # Processing time
92
+ if stats.processing_time > 0:
93
+ lines.append(f"**Processing Time:** {stats.processing_time:.2f} seconds\n")
94
+
95
+ # File list
96
+ if count <= 10:
97
+ lines.append("**Processed Files:**")
98
+ for file_path in processed_files:
99
+ lines.append(f"- `{file_path}`")
100
+ else:
101
+ lines.append("**Processed Files (first 10 shown):**")
102
+ for file_path in processed_files[:10]:
103
+ lines.append(f"- `{file_path}`")
104
+ lines.append(f"- ...and {count - 10} more.")
105
+
106
+ # Skipped files section
107
+ if skipped_files:
108
+ if not processed_files:
109
+ lines.append("No files were read and concatenated based on the criteria.\n")
110
+
111
+ skipped_count = len(skipped_files)
112
+
113
+ # Group skipped files by reason
114
+ skip_by_reason = {}
115
+ for item in skipped_files:
116
+ reason = item['reason']
117
+ if reason not in skip_by_reason:
118
+ skip_by_reason[reason] = []
119
+ skip_by_reason[reason].append(item['path'])
120
+
121
+ if skipped_count <= 10:
122
+ lines.append(f"**Skipped {skipped_count} item(s):**")
123
+ else:
124
+ lines.append(f"**Skipped {skipped_count} item(s) (first 10 shown):**")
125
+
126
+ shown_count = 0
127
+ for reason, paths in skip_by_reason.items():
128
+ if shown_count >= 10:
129
+ break
130
+
131
+ for path in paths:
132
+ if shown_count >= 10:
133
+ break
134
+ lines.append(f"- `{path}` (Reason: {reason})")
135
+ shown_count += 1
136
+
137
+ if skipped_count > 10:
138
+ lines.append(f"- ...and {skipped_count - 10} more.")
139
+
140
+ # Statistics summary
141
+ if stats.total_files_found > 0:
142
+ lines.append(f"\n**Summary:** Found {stats.total_files_found} files, "
143
+ f"processed {stats.processed_files}, "
144
+ f"skipped {stats.skipped_files}")
145
+
146
+ if stats.error_files > 0:
147
+ lines.append(f", {stats.error_files} errors")
148
+
149
+ # No results case
150
+ if not processed_files and not skipped_files:
151
+ lines.append("No files were found matching the specified criteria.")
152
+
153
+ return "\n".join(lines)
154
+
155
+ def build_filter_skip_info(self, filter_counts: Dict[str, int]) -> List[Dict]:
156
+ """
157
+ Build skip information for filtered files
158
+
159
+ Args:
160
+ filter_counts: Dictionary of filter type to count
161
+
162
+ Returns:
163
+ List of skip information dictionaries
164
+ """
165
+ skip_info = []
166
+
167
+ if filter_counts.get('git_ignored', 0) > 0:
168
+ count = filter_counts['git_ignored']
169
+ skip_info.append({
170
+ 'path': f'{count} files',
171
+ 'reason': 'Filtered by .gitignore rules'
172
+ })
173
+
174
+ return skip_info
175
+
176
+ def create_error_result(self, error_message: str) -> Dict[str, Any]:
177
+ """
178
+ Create error result
179
+
180
+ Args:
181
+ error_message: Error message to display
182
+
183
+ Returns:
184
+ Error result dictionary
185
+ """
186
+ return {
187
+ 'llmContent': [f"Error: {error_message}"],
188
+ 'returnDisplay': f"## Error\n\n{error_message}"
189
+ }
190
+
191
+ def create_info_result(self, info_message: str) -> Dict[str, Any]:
192
+ """
193
+ Create informational result
194
+
195
+ Args:
196
+ info_message: Information message to display
197
+
198
+ Returns:
199
+ Info result dictionary
200
+ """
201
+ return {
202
+ 'llmContent': [info_message],
203
+ 'returnDisplay': f"## Information\n\n{info_message}"
204
+ }