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.
- agent_config.yaml +34 -0
- siada/__init__.py +13 -0
- siada/agent_hub/__init__.py +10 -0
- siada/agent_hub/coder/bug_fix_agent.py +398 -0
- siada/agent_hub/coder/code_gen_agent.py +235 -0
- siada/agent_hub/coder/fe_gen_agent.py +37 -0
- siada/agent_hub/coder/issue_review_agent.py +68 -0
- siada/agent_hub/coder/prompt/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
- siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
- siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
- siada/agent_hub/coder/prompt/base/rules.py +40 -0
- siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
- siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
- siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
- siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
- siada/agent_hub/coder/tracing/__init__.py +22 -0
- siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
- siada/agent_hub/hooks/__init__.py +0 -0
- siada/agent_hub/hooks/io_hooks.py +30 -0
- siada/agent_hub/siada_agent.py +214 -0
- siada/config/__init__.py +0 -0
- siada/config/config_loader.py +67 -0
- siada/entrypoint/__init__.py +3 -0
- siada/entrypoint/args_parser/__init__.py +0 -0
- siada/entrypoint/args_parser/args.py +195 -0
- siada/entrypoint/interaction/__init__.py +0 -0
- siada/entrypoint/interaction/base_turn.py +107 -0
- siada/entrypoint/interaction/config.py +30 -0
- siada/entrypoint/interaction/controller.py +122 -0
- siada/entrypoint/interaction/nointeractive_controller.py +27 -0
- siada/entrypoint/interaction/turn/interface.py +116 -0
- siada/entrypoint/interaction/turn/models.py +34 -0
- siada/entrypoint/interaction/turn/run_turn.py +641 -0
- siada/entrypoint/interaction/turn_factory.py +35 -0
- siada/entrypoint/interaction/turn_models.py +28 -0
- siada/entrypoint/siadahub.py +413 -0
- siada/foundation/__init__.py +0 -0
- siada/foundation/code_agent_context.py +48 -0
- siada/foundation/config.py +59 -0
- siada/foundation/constants.py +2 -0
- siada/foundation/context.py +76 -0
- siada/foundation/logging.py +341 -0
- siada/foundation/tools/__init__.py +0 -0
- siada/foundation/tools/get_git_diff.py +105 -0
- siada/io/__init__.py +0 -0
- siada/io/banner.py +148 -0
- siada/io/color_settings.py +207 -0
- siada/io/color_utils.py +16 -0
- siada/io/components/__init__.py +0 -0
- siada/io/components/mdstream.py +204 -0
- siada/io/console_printer.py +68 -0
- siada/io/io.py +661 -0
- siada/io/key_bindings.py +129 -0
- siada/io/notification_command.py +40 -0
- siada/io/test_banner.py +64 -0
- siada/models/__init__.py +0 -0
- siada/models/agent.py +47 -0
- siada/models/coder/__init__.py +0 -0
- siada/models/converter.py +27 -0
- siada/models/model_base_config.py +121 -0
- siada/models/model_run_config.py +153 -0
- siada/provider/__init__.py +0 -0
- siada/provider/client_factory.py +112 -0
- siada/provider/llm_client.py +11 -0
- siada/provider/openrouter/__init__.py +0 -0
- siada/provider/openrouter/coverter.py +17 -0
- siada/provider/openrouter/openroute_provider.py +33 -0
- siada/provider/provider_factory.py +53 -0
- siada/queries/tree-sitter-language-pack/README.md +7 -0
- siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- siada/queries/tree-sitter-languages/README.md +23 -0
- siada/queries/tree-sitter-languages/c-tags.scm +9 -0
- siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
- siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
- siada/queries/tree-sitter-languages/go-tags.scm +30 -0
- siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- siada/queries/tree-sitter-languages/java-tags.scm +20 -0
- siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-languages/php-tags.scm +26 -0
- siada/queries/tree-sitter-languages/python-tags.scm +12 -0
- siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
- siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
- siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
- siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- siada/services/__init__.py +30 -0
- siada/services/at_file_recommendation_design_document.md +1213 -0
- siada/services/code_context_manager.py +136 -0
- siada/services/enhanced_fix_result_check.py +546 -0
- siada/services/execution_trace_collector.py +207 -0
- siada/services/file_recommendation/__init__.py +136 -0
- siada/services/file_recommendation/core/__init__.py +17 -0
- siada/services/file_recommendation/core/completion_engine.py +351 -0
- siada/services/file_recommendation/core/config.py +62 -0
- siada/services/file_recommendation/core/file_discovery.py +353 -0
- siada/services/file_recommendation/core/suggestion.py +110 -0
- siada/services/file_recommendation/utils/__init__.py +22 -0
- siada/services/file_recommendation/utils/path_utils.py +138 -0
- siada/services/file_recommendation/utils/text_utils.py +253 -0
- siada/services/fix_result_check.py +377 -0
- siada/services/handle_at_command/README.md +247 -0
- siada/services/handle_at_command/__init__.py +115 -0
- siada/services/handle_at_command/exceptions.py +46 -0
- siada/services/handle_at_command/models.py +92 -0
- siada/services/handle_at_command/parser.py +189 -0
- siada/services/handle_at_command/processor.py +321 -0
- siada/services/handle_at_command/resolver.py +318 -0
- siada/services/handle_at_command/utils.py +345 -0
- siada/services/model_info_service.py +103 -0
- siada/services/siada_runner.py +170 -0
- siada/session/__init__.py +29 -0
- siada/session/session_manager.py +66 -0
- siada/session/session_models.py +43 -0
- siada/support/__init__.py +0 -0
- siada/support/completer.py +204 -0
- siada/support/editor.py +146 -0
- siada/support/envprocessor.py +54 -0
- siada/support/repo.py +51 -0
- siada/support/slash_commands.py +410 -0
- siada/support/spinner.py +320 -0
- siada/tools/__init__.py +0 -0
- siada/tools/ast/README.md +345 -0
- siada/tools/ast/ast_tool.py +341 -0
- siada/tools/ast/models.py +11 -0
- siada/tools/coder/__init__.py +0 -0
- siada/tools/coder/ask_followup_question.py +70 -0
- siada/tools/coder/cmd_runner.py +132 -0
- siada/tools/coder/file_operator.py +284 -0
- siada/tools/coder/file_search/README.md +223 -0
- siada/tools/coder/file_search/__init__.py +10 -0
- siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
- siada/tools/coder/file_search/bin/rg.exe +0 -0
- siada/tools/coder/file_search/search.py +437 -0
- siada/tools/coder/file_search/setup_package.py +150 -0
- siada/tools/coder/file_search/test_search.py +142 -0
- siada/tools/coder/files.py +147 -0
- siada/tools/coder/fix_attempt_completion.py +58 -0
- siada/tools/coder/issue_review_completion.py +29 -0
- siada/tools/coder/observation/__init__.py +0 -0
- siada/tools/coder/observation/error.py +22 -0
- siada/tools/coder/observation/file_observation.py +216 -0
- siada/tools/coder/observation/observation.py +71 -0
- siada/tools/coder/repo_map/__init__.py +157 -0
- siada/tools/coder/repo_map/dump.py +29 -0
- siada/tools/coder/repo_map/io.py +226 -0
- siada/tools/coder/repo_map/repo_map.py +881 -0
- siada/tools/coder/repo_map/special.py +203 -0
- siada/tools/coder/repo_map/token_counter.py +154 -0
- siada/tools/coder/repo_map/waiting.py +221 -0
- siada/tools/coder/reproduce_completion.py +39 -0
- siada/tools/coder/run_cmd.py +60 -0
- siada/tools/coder/test_completion.py +25 -0
- siada/tools/coder/tool_docs.py +44 -0
- siada/tools/compression_tool.py +200 -0
- siada/tools/read_many_files/README.md +220 -0
- siada/tools/read_many_files/__init__.py +25 -0
- siada/tools/read_many_files/file_processor.py +486 -0
- siada/tools/read_many_files/filters.py +326 -0
- siada/tools/read_many_files/formatters.py +204 -0
- siada/tools/read_many_files/models.py +189 -0
- siada/tools/read_many_files_tool.py +233 -0
- siada/tools/tool_call_format/__init__.py +55 -0
- siada/tools/tool_call_format/file_to_language.py +231 -0
- siada/tools/tool_call_format/formatter_factory.py +80 -0
- siada/tools/tool_call_format/formatters.py +282 -0
- siada/tools/tool_call_format/tool_call_formatter.py +54 -0
- siada/tools/web/__init__.py +0 -0
- siada/tools/web/web_crawl.py +208 -0
- siada/utils.py +194 -0
- siada_cli-0.0.1.dist-info/LICENSE +201 -0
- siada_cli-0.0.1.dist-info/METADATA +457 -0
- siada_cli-0.0.1.dist-info/RECORD +215 -0
- siada_cli-0.0.1.dist-info/WHEEL +4 -0
- 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
|
+
}
|