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,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]