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,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())
|