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,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File discovery and filtering service.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import glob
|
|
7
|
+
import fnmatch
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .config import FilterOptions
|
|
12
|
+
from .suggestion import Suggestion, create_suggestion
|
|
13
|
+
from ..utils.path_utils import escape_path, is_hidden_file, normalize_path_separators
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitIgnoreParser:
|
|
17
|
+
"""Simple .gitignore parser"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, project_root: str):
|
|
20
|
+
self.project_root = Path(project_root).resolve()
|
|
21
|
+
self.patterns = []
|
|
22
|
+
|
|
23
|
+
def load_git_repo_patterns(self):
|
|
24
|
+
"""Load patterns from .gitignore file"""
|
|
25
|
+
gitignore_path = self.project_root / '.gitignore'
|
|
26
|
+
if gitignore_path.exists():
|
|
27
|
+
self.load_patterns_from_file(gitignore_path)
|
|
28
|
+
|
|
29
|
+
def load_patterns_from_file(self, file_path: Path):
|
|
30
|
+
"""Load patterns from a specific file"""
|
|
31
|
+
try:
|
|
32
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
33
|
+
for line in f:
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if line and not line.startswith('#'):
|
|
36
|
+
self.patterns.append(line)
|
|
37
|
+
except (IOError, UnicodeDecodeError):
|
|
38
|
+
pass # Ignore errors
|
|
39
|
+
|
|
40
|
+
def is_ignored(self, file_path: str) -> bool:
|
|
41
|
+
"""Check if file path is ignored"""
|
|
42
|
+
normalized_path = normalize_path_separators(file_path)
|
|
43
|
+
|
|
44
|
+
for pattern in self.patterns:
|
|
45
|
+
if self._match_pattern(normalized_path, pattern):
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def _match_pattern(self, file_path: str, pattern: str) -> bool:
|
|
51
|
+
"""Check if file path matches gitignore pattern"""
|
|
52
|
+
# Simple pattern matching - this could be enhanced
|
|
53
|
+
if pattern.endswith('/'):
|
|
54
|
+
# Directory pattern
|
|
55
|
+
pattern = pattern.rstrip('/')
|
|
56
|
+
return fnmatch.fnmatch(file_path, pattern) or fnmatch.fnmatch(os.path.dirname(file_path), pattern)
|
|
57
|
+
else:
|
|
58
|
+
# File pattern
|
|
59
|
+
return fnmatch.fnmatch(file_path, pattern) or fnmatch.fnmatch(os.path.basename(file_path), pattern)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FileDiscoveryService:
|
|
63
|
+
"""File discovery and filtering service"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, project_root: str):
|
|
66
|
+
self.project_root = os.path.abspath(project_root)
|
|
67
|
+
self.git_ignore_filter = None
|
|
68
|
+
|
|
69
|
+
# Initialize Git ignore filter
|
|
70
|
+
if self._is_git_repository(self.project_root):
|
|
71
|
+
self.git_ignore_filter = GitIgnoreParser(self.project_root)
|
|
72
|
+
try:
|
|
73
|
+
self.git_ignore_filter.load_git_repo_patterns()
|
|
74
|
+
except Exception:
|
|
75
|
+
pass # Ignore errors
|
|
76
|
+
|
|
77
|
+
def _is_git_repository(self, path: str) -> bool:
|
|
78
|
+
"""Check if directory is a git repository"""
|
|
79
|
+
git_dir = os.path.join(path, '.git')
|
|
80
|
+
return os.path.exists(git_dir)
|
|
81
|
+
|
|
82
|
+
def should_ignore_file(self, file_path: str, filter_options: FilterOptions) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if file should be ignored
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_path: File path to check
|
|
88
|
+
filter_options: Filter options
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if should ignore
|
|
92
|
+
"""
|
|
93
|
+
if filter_options.respect_git_ignore and self._should_git_ignore_file(file_path):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def _should_git_ignore_file(self, file_path: str) -> bool:
|
|
99
|
+
"""Check if file is ignored by git"""
|
|
100
|
+
if self.git_ignore_filter:
|
|
101
|
+
return self.git_ignore_filter.is_ignored(file_path)
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
async def find_files_recursively(
|
|
105
|
+
self,
|
|
106
|
+
start_dir: str,
|
|
107
|
+
search_prefix: str,
|
|
108
|
+
filter_options: FilterOptions,
|
|
109
|
+
current_relative_path: str = "",
|
|
110
|
+
depth: int = 0,
|
|
111
|
+
max_depth: int = 10,
|
|
112
|
+
max_results: int = 50
|
|
113
|
+
) -> List[Suggestion]:
|
|
114
|
+
"""
|
|
115
|
+
Recursively search for files and directories
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
start_dir: Starting search directory
|
|
119
|
+
search_prefix: Search prefix
|
|
120
|
+
filter_options: Filter options
|
|
121
|
+
current_relative_path: Current relative path
|
|
122
|
+
depth: Current recursion depth
|
|
123
|
+
max_depth: Maximum recursion depth
|
|
124
|
+
max_results: Maximum number of results
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List[Suggestion]: Suggestion list
|
|
128
|
+
"""
|
|
129
|
+
if depth > max_depth:
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
lower_search_prefix = search_prefix.lower()
|
|
133
|
+
found_suggestions = []
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
entries = os.listdir(start_dir)
|
|
137
|
+
|
|
138
|
+
for entry in entries:
|
|
139
|
+
if len(found_suggestions) >= max_results:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
entry_path_relative = os.path.join(current_relative_path, entry) if current_relative_path else entry
|
|
143
|
+
entry_path_from_root = os.path.relpath(
|
|
144
|
+
os.path.join(start_dir, entry),
|
|
145
|
+
self.project_root
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Skip hidden files unless search prefix starts with .
|
|
149
|
+
if not search_prefix.startswith('.') and entry.startswith('.'):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Check file filtering rules
|
|
153
|
+
if self.should_ignore_file(entry_path_from_root, filter_options):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Enhanced matching: prefix match OR fuzzy match
|
|
157
|
+
entry_matches = False
|
|
158
|
+
|
|
159
|
+
# Check prefix matching (original logic)
|
|
160
|
+
if entry.lower().startswith(lower_search_prefix):
|
|
161
|
+
entry_matches = True
|
|
162
|
+
|
|
163
|
+
# Check fuzzy matching (contains match)
|
|
164
|
+
elif self._is_fuzzy_match(entry_path_from_root, search_prefix):
|
|
165
|
+
entry_matches = True
|
|
166
|
+
|
|
167
|
+
if entry_matches:
|
|
168
|
+
full_entry_path = os.path.join(start_dir, entry)
|
|
169
|
+
is_dir = os.path.isdir(full_entry_path)
|
|
170
|
+
suffix = '/' if is_dir else ''
|
|
171
|
+
|
|
172
|
+
# Use full path from project root for suggestion
|
|
173
|
+
suggestion = create_suggestion(
|
|
174
|
+
label=entry_path_from_root + suffix,
|
|
175
|
+
value=escape_path(entry_path_from_root + suffix)
|
|
176
|
+
)
|
|
177
|
+
found_suggestions.append(suggestion)
|
|
178
|
+
|
|
179
|
+
# Recursively search subdirectories
|
|
180
|
+
full_entry_path = os.path.join(start_dir, entry)
|
|
181
|
+
if (os.path.isdir(full_entry_path) and
|
|
182
|
+
entry != 'node_modules' and
|
|
183
|
+
not entry.startswith('.') and
|
|
184
|
+
len(found_suggestions) < max_results):
|
|
185
|
+
|
|
186
|
+
sub_suggestions = await self.find_files_recursively(
|
|
187
|
+
full_entry_path,
|
|
188
|
+
search_prefix,
|
|
189
|
+
filter_options,
|
|
190
|
+
entry_path_relative,
|
|
191
|
+
depth + 1,
|
|
192
|
+
max_depth,
|
|
193
|
+
max_results - len(found_suggestions)
|
|
194
|
+
)
|
|
195
|
+
found_suggestions.extend(sub_suggestions)
|
|
196
|
+
|
|
197
|
+
except (OSError, PermissionError):
|
|
198
|
+
# Ignore permission errors and other OS errors
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
return found_suggestions[:max_results]
|
|
202
|
+
|
|
203
|
+
async def find_files_with_glob(
|
|
204
|
+
self,
|
|
205
|
+
search_prefix: str,
|
|
206
|
+
filter_options: FilterOptions,
|
|
207
|
+
search_dir: str,
|
|
208
|
+
max_results: int = 50
|
|
209
|
+
) -> List[Suggestion]:
|
|
210
|
+
"""
|
|
211
|
+
Use glob pattern to search files
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
search_prefix: Search prefix
|
|
215
|
+
filter_options: Filter options
|
|
216
|
+
search_dir: Search directory
|
|
217
|
+
max_results: Maximum number of results
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List[Suggestion]: Suggestion list
|
|
221
|
+
"""
|
|
222
|
+
glob_pattern = f"**/{search_prefix}*"
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
search_path = os.path.join(search_dir, glob_pattern)
|
|
226
|
+
files = glob.glob(search_path, recursive=True)
|
|
227
|
+
|
|
228
|
+
suggestions = []
|
|
229
|
+
for file in files[:max_results]:
|
|
230
|
+
if self.should_ignore_file(file, filter_options):
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
absolute_path = os.path.abspath(file)
|
|
234
|
+
label = os.path.relpath(absolute_path, search_dir)
|
|
235
|
+
|
|
236
|
+
# Add directory suffix
|
|
237
|
+
if os.path.isdir(absolute_path):
|
|
238
|
+
label += '/'
|
|
239
|
+
|
|
240
|
+
suggestion = create_suggestion(
|
|
241
|
+
label=label,
|
|
242
|
+
value=escape_path(label)
|
|
243
|
+
)
|
|
244
|
+
suggestions.append(suggestion)
|
|
245
|
+
|
|
246
|
+
return suggestions
|
|
247
|
+
|
|
248
|
+
except Exception:
|
|
249
|
+
# Return empty list on error
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
def find_files_in_directory(
|
|
253
|
+
self,
|
|
254
|
+
directory: str,
|
|
255
|
+
search_prefix: str,
|
|
256
|
+
filter_options: FilterOptions,
|
|
257
|
+
max_results: int = 50
|
|
258
|
+
) -> List[Suggestion]:
|
|
259
|
+
"""
|
|
260
|
+
Find files in a specific directory (non-recursive)
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
directory: Directory to search
|
|
264
|
+
search_prefix: Search prefix
|
|
265
|
+
filter_options: Filter options
|
|
266
|
+
max_results: Maximum number of results
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List[Suggestion]: Suggestion list
|
|
270
|
+
"""
|
|
271
|
+
suggestions = []
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
if not os.path.exists(directory) or not os.path.isdir(directory):
|
|
275
|
+
return suggestions
|
|
276
|
+
|
|
277
|
+
lower_search_prefix = search_prefix.lower()
|
|
278
|
+
entries = os.listdir(directory)
|
|
279
|
+
|
|
280
|
+
for entry in entries:
|
|
281
|
+
if len(suggestions) >= max_results:
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
# Skip hidden files unless search prefix starts with .
|
|
285
|
+
if not search_prefix.startswith('.') and entry.startswith('.'):
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Check file filtering rules
|
|
289
|
+
entry_path = os.path.relpath(os.path.join(directory, entry), self.project_root)
|
|
290
|
+
if self.should_ignore_file(entry_path, filter_options):
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
# Check name matching
|
|
294
|
+
if entry.lower().startswith(lower_search_prefix):
|
|
295
|
+
full_path = os.path.join(directory, entry)
|
|
296
|
+
is_dir = os.path.isdir(full_path)
|
|
297
|
+
suffix = '/' if is_dir else ''
|
|
298
|
+
|
|
299
|
+
suggestion = create_suggestion(
|
|
300
|
+
label=entry + suffix,
|
|
301
|
+
value=escape_path(entry + suffix)
|
|
302
|
+
)
|
|
303
|
+
suggestions.append(suggestion)
|
|
304
|
+
|
|
305
|
+
except (OSError, PermissionError):
|
|
306
|
+
# Ignore errors
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
return suggestions
|
|
310
|
+
|
|
311
|
+
def _is_fuzzy_match(self, file_path: str, search_term: str) -> bool:
|
|
312
|
+
"""
|
|
313
|
+
Check if file path fuzzy matches the search term
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
file_path: File path to check
|
|
317
|
+
search_term: Search term
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
bool: True if it's a fuzzy match
|
|
321
|
+
"""
|
|
322
|
+
if not search_term:
|
|
323
|
+
return False
|
|
324
|
+
|
|
325
|
+
search_term_lower = search_term.lower()
|
|
326
|
+
file_path_lower = file_path.lower()
|
|
327
|
+
|
|
328
|
+
# Check if search term is contained in file name (not full path for better UX)
|
|
329
|
+
file_name = os.path.basename(file_path_lower)
|
|
330
|
+
if search_term_lower in file_name:
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
# Check if search term is contained in full path
|
|
334
|
+
if search_term_lower in file_path_lower:
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
def get_default_ignore_patterns(self) -> List[str]:
|
|
340
|
+
"""Get default ignore patterns"""
|
|
341
|
+
return [
|
|
342
|
+
'__pycache__',
|
|
343
|
+
'.git',
|
|
344
|
+
'.vscode',
|
|
345
|
+
'.idea',
|
|
346
|
+
'node_modules',
|
|
347
|
+
'.env',
|
|
348
|
+
'*.pyc',
|
|
349
|
+
'*.pyo',
|
|
350
|
+
'*.log',
|
|
351
|
+
'.DS_Store',
|
|
352
|
+
'Thumbs.db'
|
|
353
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Suggestion data structures for file recommendation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, TypedDict, Tuple
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Suggestion(TypedDict):
|
|
10
|
+
"""
|
|
11
|
+
File suggestion data structure
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
label: Display label for the suggestion
|
|
15
|
+
value: Actual value to be inserted when selected
|
|
16
|
+
"""
|
|
17
|
+
label: str
|
|
18
|
+
value: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_suggestion(label: str, value: str = None) -> Suggestion:
|
|
22
|
+
"""
|
|
23
|
+
Create a suggestion object
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
label: Display label
|
|
27
|
+
value: Insertion value (defaults to label if not provided)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Suggestion object
|
|
31
|
+
"""
|
|
32
|
+
return Suggestion(
|
|
33
|
+
label=label,
|
|
34
|
+
value=value if value is not None else label
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def sort_suggestions(suggestions: List[Suggestion]) -> List[Suggestion]:
|
|
39
|
+
"""
|
|
40
|
+
Sort suggestions by priority:
|
|
41
|
+
1. Path depth (shallow first)
|
|
42
|
+
2. Directory type (directories first)
|
|
43
|
+
3. File name (without extension)
|
|
44
|
+
4. Full label
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
suggestions: Original suggestion list
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Sorted suggestion list
|
|
51
|
+
"""
|
|
52
|
+
def sort_key(suggestion: Suggestion) -> Tuple[int, bool, str, str]:
|
|
53
|
+
label = suggestion['label']
|
|
54
|
+
|
|
55
|
+
# 1. Calculate path depth
|
|
56
|
+
depth = label.count('/')
|
|
57
|
+
|
|
58
|
+
# 2. Check if it's a directory
|
|
59
|
+
is_dir = label.endswith('/')
|
|
60
|
+
dir_priority = 0 if is_dir else 1 # Directories first
|
|
61
|
+
|
|
62
|
+
# 3. Get filename without extension
|
|
63
|
+
basename = os.path.basename(label.rstrip('/'))
|
|
64
|
+
name_without_ext = os.path.splitext(basename)[0]
|
|
65
|
+
|
|
66
|
+
# 4. Full label name
|
|
67
|
+
full_label = label
|
|
68
|
+
|
|
69
|
+
return (depth, dir_priority, name_without_ext.lower(), full_label.lower())
|
|
70
|
+
|
|
71
|
+
return sorted(suggestions, key=sort_key)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def filter_suggestions_by_prefix(suggestions: List[Suggestion], prefix: str) -> List[Suggestion]:
|
|
75
|
+
"""
|
|
76
|
+
Filter suggestions by matching prefix
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
suggestions: List of suggestions to filter
|
|
80
|
+
prefix: Prefix to match against
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Filtered suggestions
|
|
84
|
+
"""
|
|
85
|
+
if not prefix:
|
|
86
|
+
return suggestions
|
|
87
|
+
|
|
88
|
+
prefix_lower = prefix.lower()
|
|
89
|
+
filtered = []
|
|
90
|
+
|
|
91
|
+
for suggestion in suggestions:
|
|
92
|
+
label_lower = suggestion['label'].lower()
|
|
93
|
+
if label_lower.startswith(prefix_lower):
|
|
94
|
+
filtered.append(suggestion)
|
|
95
|
+
|
|
96
|
+
return filtered
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def limit_suggestions(suggestions: List[Suggestion], max_results: int) -> List[Suggestion]:
|
|
100
|
+
"""
|
|
101
|
+
Limit the number of suggestions
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
suggestions: List of suggestions
|
|
105
|
+
max_results: Maximum number of results to return
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Limited suggestion list
|
|
109
|
+
"""
|
|
110
|
+
return suggestions[:max_results] if max_results > 0 else suggestions
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for file recommendation system.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .path_utils import escape_path, unescape_path
|
|
6
|
+
from .text_utils import (
|
|
7
|
+
is_completion_active,
|
|
8
|
+
parse_at_command_path,
|
|
9
|
+
to_code_points,
|
|
10
|
+
cp_len,
|
|
11
|
+
cp_slice
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'escape_path',
|
|
16
|
+
'unescape_path',
|
|
17
|
+
'is_completion_active',
|
|
18
|
+
'parse_at_command_path',
|
|
19
|
+
'to_code_points',
|
|
20
|
+
'cp_len',
|
|
21
|
+
'cp_slice'
|
|
22
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Path processing utilities for file recommendation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def escape_path(file_path: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Escape spaces in file paths
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
file_path: Original file path
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: Escaped path
|
|
15
|
+
"""
|
|
16
|
+
result = ""
|
|
17
|
+
for i, char in enumerate(file_path):
|
|
18
|
+
# Only escape unescaped spaces
|
|
19
|
+
if char == ' ' and (i == 0 or file_path[i-1] != '\\'):
|
|
20
|
+
result += '\\ '
|
|
21
|
+
else:
|
|
22
|
+
result += char
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def unescape_path(file_path: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Unescape spaces in file paths
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
file_path: Escaped file path
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: Unescaped path
|
|
35
|
+
"""
|
|
36
|
+
return file_path.replace('\\ ', ' ')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_path_separators(path: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Normalize path separators to forward slashes
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path: Path with potentially mixed separators
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: Path with normalized separators
|
|
48
|
+
"""
|
|
49
|
+
return path.replace('\\', '/')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_hidden_file(path: str) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Check if a file or directory is hidden (starts with .)
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: File or directory path
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
bool: True if hidden, False otherwise
|
|
61
|
+
"""
|
|
62
|
+
import os
|
|
63
|
+
basename = os.path.basename(path)
|
|
64
|
+
return basename.startswith('.')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_file_extension(path: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Get file extension from path
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: File path
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: File extension (without dot)
|
|
76
|
+
"""
|
|
77
|
+
import os
|
|
78
|
+
_, ext = os.path.splitext(path)
|
|
79
|
+
return ext.lstrip('.')
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_directory_path(path: str) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if path appears to be a directory (ends with /)
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: Path to check
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bool: True if appears to be directory path
|
|
91
|
+
"""
|
|
92
|
+
return path.endswith('/') or path.endswith('\\')
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ensure_relative_path(path: str, base_dir: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Ensure path is relative to base directory
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
path: Path to make relative
|
|
101
|
+
base_dir: Base directory
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
str: Relative path
|
|
105
|
+
"""
|
|
106
|
+
import os
|
|
107
|
+
try:
|
|
108
|
+
return os.path.relpath(path, base_dir)
|
|
109
|
+
except ValueError:
|
|
110
|
+
# Can't make relative (different drives on Windows)
|
|
111
|
+
return path
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def join_path_parts(*parts: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Join path parts with forward slashes
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
*parts: Path parts to join
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: Joined path
|
|
123
|
+
"""
|
|
124
|
+
return '/'.join(part.strip('/\\') for part in parts if part.strip('/\\'))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def split_path_parts(path: str) -> list[str]:
|
|
128
|
+
"""
|
|
129
|
+
Split path into parts
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
path: Path to split
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
list[str]: Path parts
|
|
136
|
+
"""
|
|
137
|
+
normalized = normalize_path_separators(path)
|
|
138
|
+
return [part for part in normalized.split('/') if part]
|