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,437 @@
1
+ """
2
+ Core search functionality using ripgrep binary.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import platform
8
+ import subprocess
9
+ import sys
10
+ import stat
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import List, Optional
14
+
15
+ from agents import function_tool
16
+
17
+ from siada.tools.coder.observation.observation import FunctionCallResult
18
+
19
+ # Try to import importlib.resources for packaged environments
20
+ try:
21
+ from importlib import resources
22
+ except ImportError:
23
+ try:
24
+ import importlib_resources as resources
25
+ except ImportError:
26
+ resources = None
27
+
28
+
29
+ @dataclass
30
+ class SearchResult:
31
+ """Represents a single search result with context."""
32
+ file_path: str
33
+ line: int
34
+ column: int
35
+ match: str
36
+ before_context: List[str]
37
+ after_context: List[str]
38
+
39
+
40
+ class RipgrepSearchResult(FunctionCallResult):
41
+ """Represents a single search result with context."""
42
+ search_results: List[SearchResult]
43
+ cwd: str
44
+
45
+ def __init__(self, search_results: List[SearchResult], cwd: str):
46
+ self.search_results = search_results
47
+ self.cwd = cwd
48
+ # super().__init__(content=content)
49
+
50
+
51
+ @property
52
+ def content(self) -> str:
53
+ """Generate content dynamically from search results."""
54
+ if self.search_results:
55
+ return RipgrepSearcher.format_results(self.search_results, self.cwd)
56
+ else:
57
+ return "No results found"
58
+
59
+ def format_for_display(self):
60
+ if self.search_results:
61
+ match_term = "match" if len(self.search_results) == 1 else "matches"
62
+ return f"Found {len(self.search_results)} {match_term}."
63
+ else:
64
+ return "No results found"
65
+
66
+ def __str__(self):
67
+ return self.content
68
+
69
+
70
+ class RipgrepSearcher:
71
+ """High-performance file search using ripgrep binary."""
72
+
73
+ MAX_RESULTS = 300
74
+
75
+ def __init__(self):
76
+ """Initialize the searcher and locate ripgrep binary."""
77
+ self.rg_path = self._find_ripgrep_binary()
78
+ if not self.rg_path:
79
+ raise RuntimeError("Could not find ripgrep binary")
80
+
81
+ def _find_ripgrep_binary(self) -> Optional[str]:
82
+ """
83
+ Find ripgrep binary in multiple possible locations.
84
+ Supports both development and packaged environments.
85
+ """
86
+ # Determine binary name based on platform
87
+ system = platform.system().lower()
88
+ machine = platform.machine().lower()
89
+
90
+ # Map platform to binary names
91
+ binary_names = []
92
+
93
+ if system == "windows":
94
+ binary_names = ["rg.exe", "rg-windows.exe"]
95
+ elif system == "darwin": # macOS
96
+ if machine in ["arm64", "aarch64"]:
97
+ binary_names = ["rg-macos-arm64", "rg-macos", "rg"]
98
+ else:
99
+ binary_names = ["rg-macos-x64", "rg-macos", "rg"]
100
+ elif system == "linux":
101
+ if machine in ["arm64", "aarch64"]:
102
+ binary_names = ["rg-linux-arm64", "rg-linux", "rg"]
103
+ else:
104
+ binary_names = ["rg-linux-x64", "rg-linux", "rg"]
105
+ else:
106
+ binary_names = ["rg"]
107
+
108
+ # Check environment variable path first
109
+ env_path = os.environ.get('RIPGREP_BINARY_PATH')
110
+ if env_path and os.path.exists(env_path) and os.access(env_path, os.X_OK):
111
+ return env_path
112
+
113
+ # Try using importlib.resources for packaged environments
114
+ if resources:
115
+ try:
116
+ with resources.path('siada.tools.coder.file_search', 'bin') as bin_path:
117
+ for binary_name in binary_names:
118
+ binary_file = bin_path / binary_name
119
+ if binary_file.exists():
120
+ self._ensure_executable(binary_file)
121
+ if os.access(binary_file, os.X_OK):
122
+ return str(binary_file)
123
+ except (ImportError, FileNotFoundError, AttributeError):
124
+ pass
125
+
126
+ # Check multiple possible search paths
127
+ search_paths = [
128
+ Path(__file__).parent / "bin", # Development environment
129
+ Path(sys.executable).parent / "ripgrep_bin", # Installation environment
130
+ Path.home() / ".local" / "bin" / "ripgrep", # User directory
131
+ Path("/usr/local/bin"), # System paths
132
+ Path("/opt/homebrew/bin"),
133
+ Path("C:/Program Files/ripgrep/bin"), # Windows common paths
134
+ Path("C:/tools/ripgrep"),
135
+ ]
136
+
137
+ # Add paths from PATH environment variable
138
+ path_env = os.environ.get('PATH', '')
139
+ for path_str in path_env.split(os.pathsep):
140
+ if path_str.strip():
141
+ search_paths.append(Path(path_str.strip()))
142
+
143
+ # Search for binary files in all paths
144
+ for search_path in search_paths:
145
+ if not search_path.exists():
146
+ continue
147
+
148
+ for binary_name in binary_names:
149
+ binary_path = search_path / binary_name
150
+ if binary_path.exists():
151
+ self._ensure_executable(binary_path)
152
+ if os.access(binary_path, os.X_OK):
153
+ return str(binary_path)
154
+
155
+ return None
156
+
157
+ def _ensure_executable(self, binary_path: Path) -> None:
158
+ """
159
+ Ensure binary file has execute permissions.
160
+ In some packaged environments, files may lose execute permissions.
161
+ """
162
+ try:
163
+ current_mode = binary_path.stat().st_mode
164
+ new_mode = current_mode | stat.S_IXUSR # Add user execute permission
165
+
166
+ # Add execute permission if read permission exists
167
+ if current_mode & stat.S_IRGRP:
168
+ new_mode |= stat.S_IXGRP
169
+ if current_mode & stat.S_IROTH:
170
+ new_mode |= stat.S_IXOTH
171
+
172
+ if new_mode != current_mode:
173
+ os.chmod(binary_path, new_mode)
174
+ except (OSError, PermissionError):
175
+ # Ignore errors if unable to modify permissions
176
+ pass
177
+
178
+ def _execute_ripgrep(self, args: List[str]) -> str:
179
+ """
180
+ Execute ripgrep with given arguments and return output.
181
+ Implements output limiting similar to the original TypeScript version.
182
+ """
183
+ try:
184
+ process = subprocess.Popen(
185
+ [self.rg_path] + args,
186
+ stdout=subprocess.PIPE,
187
+ stderr=subprocess.PIPE,
188
+ text=True,
189
+ bufsize=1,
190
+ universal_newlines=True
191
+ )
192
+
193
+ output_lines = []
194
+ max_lines = self.MAX_RESULTS * 5
195
+
196
+ for line in process.stdout:
197
+ if len(output_lines) < max_lines:
198
+ output_lines.append(line.rstrip('\n\r'))
199
+ else:
200
+ process.terminate()
201
+ break
202
+
203
+ process.wait()
204
+
205
+ # Return code 1 means no matches found, which is normal
206
+ if process.returncode != 0 and process.returncode != 1:
207
+ stderr_output = process.stderr.read() if process.stderr else ""
208
+ if stderr_output.strip():
209
+ raise subprocess.CalledProcessError(
210
+ process.returncode,
211
+ [self.rg_path] + args,
212
+ stderr_output
213
+ )
214
+
215
+ return '\n'.join(output_lines)
216
+
217
+ except subprocess.CalledProcessError as e:
218
+ raise RuntimeError(f"ripgrep process error: {e.stderr}")
219
+ except Exception as e:
220
+ raise RuntimeError(f"ripgrep execution failed: {str(e)}")
221
+
222
+ def _parse_ripgrep_output(self, output: str) -> List[SearchResult]:
223
+ """
224
+ Parse ripgrep JSON output into SearchResult objects.
225
+ Handles both match and context lines.
226
+ """
227
+ results = []
228
+ current_result = None
229
+
230
+ for line in output.split('\n'):
231
+ if not line.strip():
232
+ continue
233
+
234
+ try:
235
+ parsed = json.loads(line)
236
+
237
+ if parsed.get('type') == 'match':
238
+ if current_result:
239
+ results.append(current_result)
240
+
241
+ data = parsed.get('data', {})
242
+ path_info = data.get('path', {})
243
+ submatches = data.get('submatches', [{}])
244
+ lines_info = data.get('lines', {})
245
+
246
+ current_result = SearchResult(
247
+ file_path=path_info.get('text', ''),
248
+ line=data.get('line_number', 0),
249
+ column=submatches[0].get('start', 0) if submatches else 0,
250
+ match=lines_info.get('text', ''),
251
+ before_context=[],
252
+ after_context=[]
253
+ )
254
+
255
+ elif parsed.get('type') == 'context' and current_result:
256
+ data = parsed.get('data', {})
257
+ context_line_number = data.get('line_number', 0)
258
+ context_text = data.get('lines', {}).get('text', '')
259
+
260
+ if context_line_number < current_result.line:
261
+ current_result.before_context.append(context_text)
262
+ else:
263
+ current_result.after_context.append(context_text)
264
+
265
+ except json.JSONDecodeError:
266
+ continue
267
+ except Exception:
268
+ continue
269
+
270
+ if current_result:
271
+ results.append(current_result)
272
+
273
+ return results
274
+
275
+ def search_in_files(
276
+ self,
277
+ directory_path: str,
278
+ regex: str,
279
+ file_pattern: str = "*",
280
+ cwd: Optional[str] = None
281
+ ) -> RipgrepSearchResult:
282
+ """
283
+ Perform regex search in files and return formatted results.
284
+
285
+ Args:
286
+ directory_path: Directory to search in
287
+ regex: Regular expression pattern (Rust regex syntax)
288
+ file_pattern: Glob pattern to filter files (default: "*")
289
+ cwd: Current working directory for relative path calculation
290
+
291
+ Returns:
292
+ Formatted search results as string
293
+ """
294
+ # Build ripgrep arguments
295
+ args = [
296
+ "--json",
297
+ "-e", regex,
298
+ "--glob", file_pattern,
299
+ "--context", "1",
300
+ directory_path
301
+ ]
302
+
303
+ try:
304
+ output = self._execute_ripgrep(args)
305
+
306
+ if not output.strip():
307
+ return "No results found"
308
+
309
+ results = self._parse_ripgrep_output(output)
310
+
311
+ return RipgrepSearchResult(search_results=results, cwd=cwd or os.getcwd())
312
+
313
+ except Exception:
314
+ return RipgrepSearchResult(search_results=[], cwd=cwd or os.getcwd())
315
+
316
+ @staticmethod
317
+ def format_results(results: List[SearchResult], cwd: str) -> str:
318
+ """
319
+ Format search results into readable string output.
320
+ Mimics the original TypeScript formatting.
321
+ """
322
+ grouped_results = {}
323
+
324
+ for result in results[:RipgrepSearcher.MAX_RESULTS]:
325
+ try:
326
+ relative_path = os.path.relpath(result.file_path, cwd)
327
+ except ValueError:
328
+ # Handle case where paths are on different drives (Windows)
329
+ relative_path = result.file_path
330
+
331
+ relative_path = relative_path.replace('\\', '/')
332
+
333
+ if relative_path not in grouped_results:
334
+ grouped_results[relative_path] = []
335
+ grouped_results[relative_path].append(result)
336
+
337
+ output_lines = []
338
+
339
+ total_results = len(results)
340
+ if total_results >= RipgrepSearcher.MAX_RESULTS:
341
+ output_lines.append(f"Showing first {RipgrepSearcher.MAX_RESULTS} of {RipgrepSearcher.MAX_RESULTS}+ results. Use a more specific search if necessary.")
342
+ else:
343
+ result_word = "result" if total_results == 1 else "results"
344
+ output_lines.append(f"Found {total_results:,} {result_word}.")
345
+
346
+ output_lines.append("")
347
+
348
+ for file_path, file_results in grouped_results.items():
349
+ output_lines.append(file_path)
350
+ output_lines.append("│----")
351
+
352
+ for i, result in enumerate(file_results):
353
+ all_lines = (
354
+ result.before_context +
355
+ [result.match] +
356
+ result.after_context
357
+ )
358
+
359
+ for line in all_lines:
360
+ formatted_line = line.rstrip() if line else ""
361
+ output_lines.append(f"│{formatted_line}")
362
+
363
+ if i < len(file_results) - 1:
364
+ output_lines.append("│----")
365
+
366
+ output_lines.append("│----")
367
+ output_lines.append("")
368
+
369
+ return '\n'.join(output_lines).rstrip()
370
+
371
+
372
+ @function_tool(
373
+ name_override="regex_search_files"
374
+ )
375
+ def regex_search_files(
376
+ cwd: str,
377
+ directory_path: str,
378
+ regex: str,
379
+ file_pattern: str = "*"
380
+ ) -> FunctionCallResult:
381
+ """
382
+ Perform high-performance regex search across files using ripgrep.
383
+
384
+ This function provides a convenient interface to search for patterns in files
385
+ within a specified directory. It uses ripgrep for fast searching and returns
386
+ formatted results with context lines for better readability.
387
+
388
+ Args:
389
+ cwd (str): Current working directory used as the base for calculating
390
+ relative file paths in the output. This helps make the results
391
+ more readable by showing paths relative to the project root.
392
+ directory_path (str): The target directory to search in. Can be an absolute
393
+ or relative path. All files matching the file_pattern
394
+ within this directory (and subdirectories) will be searched.
395
+ regex (str): Regular expression pattern to search for. Uses Rust regex syntax
396
+ which is similar to PCRE. Supports advanced features like lookahead,
397
+ lookbehind, and Unicode character classes.
398
+ file_pattern (str, optional): Glob pattern to filter which files to search.
399
+ Defaults to "*" (all files). Examples:
400
+ - "*.py" for Python files only
401
+ - "*.{js,ts}" for JavaScript and TypeScript files
402
+ - "test_*.py" for Python test files
403
+
404
+ Returns:
405
+ str: Formatted search results containing:
406
+ - Summary line with total number of matches found
407
+ - For each file with matches:
408
+ - Relative file path
409
+ - Each match with surrounding context lines
410
+ - Line numbers and column positions
411
+ - Results are limited to MAX_RESULTS (300) for performance
412
+ - Returns "No results found" if no matches are discovered
413
+
414
+ Raises:
415
+ RuntimeError: If the ripgrep binary cannot be found or executed.
416
+ This can happen if ripgrep is not installed or the binary
417
+ path is not properly configured.
418
+
419
+ Example:
420
+ >>> results = regex_search_files(
421
+ ... cwd="/project/root",
422
+ ... directory_path="siada",
423
+ ... regex=r"def\\s+(\\w+)",
424
+ ... file_pattern="*.py"
425
+ ... )
426
+ >>> print(results)
427
+ Found 15 results.
428
+
429
+ siada/main.py
430
+ │----
431
+ │class MyClass:
432
+ │ def my_function(self):
433
+ │ pass
434
+ │----
435
+ """
436
+ searcher = RipgrepSearcher()
437
+ return searcher.search_in_files(directory_path, regex, file_pattern, cwd)
@@ -0,0 +1,150 @@
1
+ """
2
+ Package configuration helper script.
3
+ Ensures ripgrep binary files are properly included during packaging.
4
+ """
5
+
6
+ import os
7
+ import stat
8
+ from pathlib import Path
9
+
10
+
11
+ def setup_package_data():
12
+ """
13
+ Setup package data configuration for distribution.
14
+ Returns configuration for setup.py or pyproject.toml.
15
+ """
16
+ package_data = {
17
+ 'siada.tools.coder.file_search': [
18
+ 'bin/*',
19
+ 'README.md',
20
+ ]
21
+ }
22
+
23
+ return package_data
24
+
25
+
26
+ def ensure_binary_permissions():
27
+ """
28
+ Ensure all binary files have correct execution permissions.
29
+ Call this function before packaging.
30
+ """
31
+ bin_dir = Path(__file__).parent / "bin"
32
+
33
+ if not bin_dir.exists():
34
+ print(f"Warning: bin directory does not exist: {bin_dir}")
35
+ return
36
+
37
+ binary_files = [
38
+ "rg.exe",
39
+ "rg-macos-arm64",
40
+ "rg-macos-x64",
41
+ "rg-linux-arm64",
42
+ "rg-linux-x64"
43
+ ]
44
+
45
+ for binary_name in binary_files:
46
+ binary_path = bin_dir / binary_name
47
+ if binary_path.exists():
48
+ try:
49
+ current_mode = binary_path.stat().st_mode
50
+ new_mode = current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
51
+ os.chmod(binary_path, new_mode)
52
+ print(f"✓ Set execution permissions: {binary_path}")
53
+ except (OSError, PermissionError) as e:
54
+ print(f"✗ Failed to set permissions for {binary_path}: {e}")
55
+ else:
56
+ print(f"⚠ Binary file not found: {binary_path}")
57
+
58
+
59
+ def get_pyproject_toml_config():
60
+ """
61
+ Returns configuration snippet for pyproject.toml.
62
+ """
63
+ config = '''
64
+ [tool.setuptools.package-data]
65
+ "siada.tools.coder.file_search" = ["bin/*", "README.md"]
66
+
67
+ [tool.setuptools.packages.find]
68
+ where = ["siada"]
69
+ include = ["siada.tools.coder.file_search*"]
70
+ '''
71
+ return config
72
+
73
+
74
+ def get_setup_py_config():
75
+ """
76
+ Returns configuration snippet for setup.py.
77
+ """
78
+ config = '''
79
+ package_data={
80
+ 'siada.tools.coder.file_search': [
81
+ 'bin/*',
82
+ 'README.md',
83
+ ],
84
+ },
85
+ include_package_data=True,
86
+ '''
87
+ return config
88
+
89
+
90
+ def validate_package_structure():
91
+ """
92
+ Validate package structure is correct.
93
+ """
94
+ base_dir = Path(__file__).parent
95
+ required_files = [
96
+ "__init__.py",
97
+ "search.py",
98
+ "README.md",
99
+ ]
100
+
101
+ required_dirs = [
102
+ "bin",
103
+ ]
104
+
105
+ print("Validating package structure...")
106
+
107
+ for file_name in required_files:
108
+ file_path = base_dir / file_name
109
+ if file_path.exists():
110
+ print(f"✓ {file_name}")
111
+ else:
112
+ print(f"✗ Missing file: {file_name}")
113
+
114
+ for dir_name in required_dirs:
115
+ dir_path = base_dir / dir_name
116
+ if dir_path.exists() and dir_path.is_dir():
117
+ print(f"✓ {dir_name}/")
118
+ if dir_name == "bin":
119
+ bin_files = list(dir_path.glob("*"))
120
+ if bin_files:
121
+ print(f" Contains {len(bin_files)} binary files")
122
+ for bin_file in bin_files:
123
+ if bin_file.is_file():
124
+ print(f" - {bin_file.name}")
125
+ else:
126
+ print(f" ⚠ bin directory is empty")
127
+ else:
128
+ print(f"✗ Missing directory: {dir_name}/")
129
+
130
+
131
+ if __name__ == "__main__":
132
+ print("File Search - Package Configuration Check")
133
+ print("=" * 40)
134
+
135
+ validate_package_structure()
136
+
137
+ print("\n" + "=" * 40)
138
+
139
+ ensure_binary_permissions()
140
+
141
+ print("\n" + "=" * 40)
142
+ print("Package configuration info:")
143
+ print("\n1. pyproject.toml config:")
144
+ print(get_pyproject_toml_config())
145
+
146
+ print("\n2. setup.py config:")
147
+ print(get_setup_py_config())
148
+
149
+ print("\n3. Package data config:")
150
+ print(setup_package_data())