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,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AtCommand Processor - Main processor that coordinates all components.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import re
|
|
7
|
+
from typing import List, Dict, Optional, Any, Tuple
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
HandleAtCommandParams,
|
|
11
|
+
HandleAtCommandResult,
|
|
12
|
+
AtCommandPart,
|
|
13
|
+
ResolverContext,
|
|
14
|
+
ProcessingStats,
|
|
15
|
+
IgnoredFileStats
|
|
16
|
+
)
|
|
17
|
+
from .parser import AtCommandParser
|
|
18
|
+
from .resolver import PathResolver
|
|
19
|
+
from .exceptions import AtCommandError
|
|
20
|
+
|
|
21
|
+
# Import ReadManyFiles tool
|
|
22
|
+
from siada.tools.read_many_files_tool import ReadManyFilesTool
|
|
23
|
+
from siada.tools.read_many_files.models import ReadManyFilesParams
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AtCommandProcessor:
|
|
27
|
+
"""Main processor for handling @ commands"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config=None):
|
|
30
|
+
self.config = config
|
|
31
|
+
self.parser = AtCommandParser()
|
|
32
|
+
self.stats = ProcessingStats()
|
|
33
|
+
self.ignored_stats = IgnoredFileStats()
|
|
34
|
+
|
|
35
|
+
# Regex to parse file content from read_many_files output
|
|
36
|
+
self.file_content_regex = re.compile(r'^--- (.*?) ---\n\n([\s\S]*?)\n\n$')
|
|
37
|
+
|
|
38
|
+
async def handle_at_command(self, params: HandleAtCommandParams) -> HandleAtCommandResult:
|
|
39
|
+
"""
|
|
40
|
+
Main entry point for handling @ commands
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
params: HandleAtCommandParams with all necessary parameters
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
HandleAtCommandResult with processed query and status
|
|
47
|
+
"""
|
|
48
|
+
start_time = time.time()
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# 1. Parse user input
|
|
52
|
+
command_parts = self.parser.parse_all_at_commands(params.query)
|
|
53
|
+
at_path_parts = [part for part in command_parts if part.type == 'atPath']
|
|
54
|
+
|
|
55
|
+
self.stats.total_at_commands = len(at_path_parts)
|
|
56
|
+
|
|
57
|
+
# 2. Early exit if no @ commands
|
|
58
|
+
if not at_path_parts:
|
|
59
|
+
params.add_item({'type': 'user', 'text': params.query}, params.message_id)
|
|
60
|
+
return HandleAtCommandResult([{'text': params.query}], True)
|
|
61
|
+
|
|
62
|
+
params.add_item({'type': 'user', 'text': params.query}, params.message_id)
|
|
63
|
+
|
|
64
|
+
# 3. Initialize resolver context
|
|
65
|
+
resolver_context = self._create_resolver_context(params.config)
|
|
66
|
+
resolver = PathResolver(resolver_context)
|
|
67
|
+
|
|
68
|
+
# 4. Resolve paths
|
|
69
|
+
paths_to_read = []
|
|
70
|
+
at_path_to_resolved_map = {}
|
|
71
|
+
content_labels = []
|
|
72
|
+
|
|
73
|
+
for at_path_part in at_path_parts:
|
|
74
|
+
try:
|
|
75
|
+
resolution_result = await resolver.resolve_path(
|
|
76
|
+
at_path_part.content,
|
|
77
|
+
params.on_debug_message
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if resolution_result.resolved_path:
|
|
81
|
+
paths_to_read.append(resolution_result.resolved_path)
|
|
82
|
+
at_path_to_resolved_map[at_path_part.content] = resolution_result.resolved_path
|
|
83
|
+
content_labels.append(at_path_part.content[1:]) # Remove @
|
|
84
|
+
self.stats.resolved_paths += 1
|
|
85
|
+
else:
|
|
86
|
+
params.on_debug_message(f'Failed to resolve {at_path_part.content}: {resolution_result.reason}')
|
|
87
|
+
self.stats.failed_paths += 1
|
|
88
|
+
|
|
89
|
+
except AtCommandError as e:
|
|
90
|
+
params.on_debug_message(f'Error resolving {at_path_part.content}: {e}')
|
|
91
|
+
self.stats.failed_paths += 1
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# 5. Handle case with no valid paths
|
|
95
|
+
if not paths_to_read:
|
|
96
|
+
params.on_debug_message('No valid file paths found in @ commands to read.')
|
|
97
|
+
initial_query = self._rebuild_initial_query(command_parts, at_path_to_resolved_map)
|
|
98
|
+
return HandleAtCommandResult([{'text': initial_query or params.query}], True)
|
|
99
|
+
|
|
100
|
+
# 6. Read files using ReadManyFilesTool
|
|
101
|
+
try:
|
|
102
|
+
file_contents = await self._read_files(
|
|
103
|
+
paths_to_read,
|
|
104
|
+
resolver_context.target_directory,
|
|
105
|
+
resolver_context.file_filtering_options,
|
|
106
|
+
params.signal
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.stats.files_read = len(file_contents) if file_contents else 0
|
|
110
|
+
|
|
111
|
+
# 7. Build processed query
|
|
112
|
+
processed_query = self._build_processed_query(
|
|
113
|
+
command_parts,
|
|
114
|
+
at_path_to_resolved_map,
|
|
115
|
+
file_contents
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# 8. Record success
|
|
119
|
+
self._record_tool_success(content_labels, params.add_item, params.message_id)
|
|
120
|
+
|
|
121
|
+
# 9. Update processing time
|
|
122
|
+
self.stats.processing_time = time.time() - start_time
|
|
123
|
+
|
|
124
|
+
return HandleAtCommandResult(processed_query, True)
|
|
125
|
+
|
|
126
|
+
except Exception as error:
|
|
127
|
+
# 10. Handle file reading errors
|
|
128
|
+
self._handle_read_error(error, content_labels, params.add_item, params.message_id)
|
|
129
|
+
return HandleAtCommandResult(None, False)
|
|
130
|
+
|
|
131
|
+
except Exception as error:
|
|
132
|
+
# Handle unexpected errors
|
|
133
|
+
params.on_debug_message(f'Unexpected error in handleAtCommand: {error}')
|
|
134
|
+
params.add_item(
|
|
135
|
+
{'type': 'error', 'text': f'Unexpected error processing @ commands: {str(error)}'},
|
|
136
|
+
params.message_id
|
|
137
|
+
)
|
|
138
|
+
return HandleAtCommandResult(None, False)
|
|
139
|
+
|
|
140
|
+
def _create_resolver_context(self, config) -> ResolverContext:
|
|
141
|
+
"""
|
|
142
|
+
Create resolver context from config
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
config: Configuration object
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
ResolverContext for path resolution
|
|
149
|
+
"""
|
|
150
|
+
# Extract configuration values
|
|
151
|
+
# This is a simplified implementation - in practice, you'd extract from actual config
|
|
152
|
+
workspace_directories = [config.root_dir] if hasattr(config, 'root_dir') and config.root_dir else ['.']
|
|
153
|
+
target_directory = config.root_dir if hasattr(config, 'root_dir') and config.root_dir else '.'
|
|
154
|
+
|
|
155
|
+
return ResolverContext(
|
|
156
|
+
workspace_directories=workspace_directories,
|
|
157
|
+
target_directory=target_directory,
|
|
158
|
+
enable_recursive_search=True,
|
|
159
|
+
file_filtering_options={
|
|
160
|
+
'respect_git_ignore': True
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def _read_files(self, paths: List[str], target_dir: str,
|
|
165
|
+
filtering_options: Dict[str, bool], signal=None) -> List[Any]:
|
|
166
|
+
"""
|
|
167
|
+
Read files using ReadManyFilesTool
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
paths: List of file paths to read
|
|
171
|
+
target_dir: Target directory
|
|
172
|
+
filtering_options: File filtering options
|
|
173
|
+
signal: Cancellation signal
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
List of file contents
|
|
177
|
+
"""
|
|
178
|
+
tool = ReadManyFilesTool(target_dir)
|
|
179
|
+
|
|
180
|
+
params = ReadManyFilesParams(
|
|
181
|
+
paths=paths,
|
|
182
|
+
file_filtering_options=filtering_options
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
result = await tool.execute(params, signal)
|
|
186
|
+
|
|
187
|
+
if result and hasattr(result, 'llmContent'):
|
|
188
|
+
return result.llmContent
|
|
189
|
+
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
def _rebuild_initial_query(self, command_parts: List[AtCommandPart],
|
|
193
|
+
at_path_to_resolved_map: Dict[str, str]) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Rebuild the initial query text with resolved paths
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
command_parts: Parsed command parts
|
|
199
|
+
at_path_to_resolved_map: Mapping of @ paths to resolved paths
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Rebuilt query string
|
|
203
|
+
"""
|
|
204
|
+
initial_query_text = ""
|
|
205
|
+
|
|
206
|
+
for i, part in enumerate(command_parts):
|
|
207
|
+
if part.type == 'text':
|
|
208
|
+
initial_query_text += part.content
|
|
209
|
+
else: # atPath
|
|
210
|
+
resolved_spec = at_path_to_resolved_map.get(part.content)
|
|
211
|
+
|
|
212
|
+
# Add appropriate spacing
|
|
213
|
+
if (i > 0 and initial_query_text and
|
|
214
|
+
not initial_query_text.endswith(' ') and
|
|
215
|
+
(resolved_spec or not part.content.startswith(' '))):
|
|
216
|
+
initial_query_text += ' '
|
|
217
|
+
|
|
218
|
+
if resolved_spec:
|
|
219
|
+
initial_query_text += f'@{resolved_spec}'
|
|
220
|
+
else:
|
|
221
|
+
initial_query_text += part.content
|
|
222
|
+
|
|
223
|
+
return initial_query_text.strip()
|
|
224
|
+
|
|
225
|
+
def _build_processed_query(self, command_parts: List[AtCommandPart],
|
|
226
|
+
at_path_to_resolved_map: Dict[str, str],
|
|
227
|
+
file_contents: List[Any]) -> List[Dict]:
|
|
228
|
+
"""
|
|
229
|
+
Build the processed query with file contents injected
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
command_parts: Parsed command parts
|
|
233
|
+
at_path_to_resolved_map: Mapping of @ paths to resolved paths
|
|
234
|
+
file_contents: File contents from ReadManyFilesTool
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of processed query parts
|
|
238
|
+
"""
|
|
239
|
+
# 1. Rebuild initial query text
|
|
240
|
+
initial_query_text = self._rebuild_initial_query(command_parts, at_path_to_resolved_map)
|
|
241
|
+
|
|
242
|
+
# 2. Start with the initial query
|
|
243
|
+
processed_parts = [{'text': initial_query_text}]
|
|
244
|
+
|
|
245
|
+
# 3. Add file contents if any
|
|
246
|
+
if file_contents:
|
|
247
|
+
processed_parts.append({'text': '\n--- Content from referenced files ---'})
|
|
248
|
+
|
|
249
|
+
for file_content_part in file_contents:
|
|
250
|
+
if isinstance(file_content_part, str):
|
|
251
|
+
# Parse file content format: "--- filepath ---\n\ncontent\n\n"
|
|
252
|
+
file_path, content = self.parser.extract_file_content_info(file_content_part)
|
|
253
|
+
|
|
254
|
+
if file_path:
|
|
255
|
+
processed_parts.append({'text': f'\nContent from @{file_path}:\n'})
|
|
256
|
+
processed_parts.append({'text': content})
|
|
257
|
+
else:
|
|
258
|
+
processed_parts.append({'text': file_content_part})
|
|
259
|
+
else:
|
|
260
|
+
# Non-string content (e.g., image Part objects)
|
|
261
|
+
processed_parts.append(file_content_part)
|
|
262
|
+
|
|
263
|
+
processed_parts.append({'text': '\n--- End of content ---'})
|
|
264
|
+
|
|
265
|
+
return processed_parts
|
|
266
|
+
|
|
267
|
+
def _record_tool_success(self, content_labels: List[str], add_item: callable, message_id: int):
|
|
268
|
+
"""
|
|
269
|
+
Record successful tool execution
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
content_labels: List of content labels
|
|
273
|
+
add_item: Function to add items to history
|
|
274
|
+
message_id: Message ID
|
|
275
|
+
"""
|
|
276
|
+
if content_labels:
|
|
277
|
+
success_message = f"Successfully read {len(content_labels)} file(s): {', '.join(content_labels)}"
|
|
278
|
+
add_item(
|
|
279
|
+
{'type': 'tool_success', 'text': success_message},
|
|
280
|
+
message_id
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _handle_read_error(self, error: Exception, content_labels: List[str],
|
|
284
|
+
add_item: callable, message_id: int):
|
|
285
|
+
"""
|
|
286
|
+
Handle file reading errors
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
error: Exception that occurred
|
|
290
|
+
content_labels: List of content labels that were being processed
|
|
291
|
+
add_item: Function to add items to history
|
|
292
|
+
message_id: Message ID
|
|
293
|
+
"""
|
|
294
|
+
error_message = f"Error reading files {', '.join(content_labels)}: {str(error)}"
|
|
295
|
+
add_item(
|
|
296
|
+
{'type': 'error', 'text': error_message},
|
|
297
|
+
message_id
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def get_processing_stats(self) -> ProcessingStats:
|
|
301
|
+
"""
|
|
302
|
+
Get processing statistics
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
ProcessingStats object
|
|
306
|
+
"""
|
|
307
|
+
return self.stats
|
|
308
|
+
|
|
309
|
+
def get_ignored_stats(self) -> IgnoredFileStats:
|
|
310
|
+
"""
|
|
311
|
+
Get ignored file statistics
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
IgnoredFileStats object
|
|
315
|
+
"""
|
|
316
|
+
return self.ignored_stats
|
|
317
|
+
|
|
318
|
+
def reset_stats(self):
|
|
319
|
+
"""Reset processing statistics"""
|
|
320
|
+
self.stats = ProcessingStats()
|
|
321
|
+
self.ignored_stats = IgnoredFileStats()
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Path Resolver - Resolves @ command paths to actual file paths.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import glob
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from .models import ResolverContext, PathResolutionResult
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
PathNotFoundError,
|
|
13
|
+
WorkspaceSecurityError,
|
|
14
|
+
PathIgnoredError,
|
|
15
|
+
InvalidPathError
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PathResolver:
|
|
20
|
+
"""Resolves @ command paths to actual file paths"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, context: ResolverContext):
|
|
23
|
+
self.context = context
|
|
24
|
+
self.target_dir = Path(context.target_directory).resolve()
|
|
25
|
+
|
|
26
|
+
async def resolve_path(self, at_path: str, on_debug_message: callable = None) -> PathResolutionResult:
|
|
27
|
+
"""
|
|
28
|
+
Resolve an @ path to an actual file path
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
at_path: @ path string (e.g., "@file.py", "@directory/")
|
|
32
|
+
on_debug_message: Optional debug message callback
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
PathResolutionResult with resolution details
|
|
36
|
+
"""
|
|
37
|
+
if not on_debug_message:
|
|
38
|
+
on_debug_message = lambda msg: None
|
|
39
|
+
|
|
40
|
+
# Handle lone '@' symbol
|
|
41
|
+
if at_path == '@':
|
|
42
|
+
on_debug_message('Lone @ detected, treating as text')
|
|
43
|
+
return PathResolutionResult(
|
|
44
|
+
resolved_path=None,
|
|
45
|
+
original_path=at_path,
|
|
46
|
+
resolution_type='not_found',
|
|
47
|
+
reason='Lone @ symbol treated as text'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Extract path without '@' prefix
|
|
51
|
+
if not at_path.startswith('@'):
|
|
52
|
+
raise InvalidPathError(f"Invalid @ path format: {at_path}")
|
|
53
|
+
|
|
54
|
+
path_name = at_path[1:] # Remove '@' prefix
|
|
55
|
+
|
|
56
|
+
if not path_name:
|
|
57
|
+
return PathResolutionResult(
|
|
58
|
+
resolved_path=None,
|
|
59
|
+
original_path=at_path,
|
|
60
|
+
resolution_type='not_found',
|
|
61
|
+
reason='Empty path after @'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Security validation
|
|
65
|
+
if not self._is_path_safe(path_name):
|
|
66
|
+
raise WorkspaceSecurityError(f"Path contains unsafe elements: {path_name}")
|
|
67
|
+
|
|
68
|
+
# Try to resolve path in each workspace directory
|
|
69
|
+
for workspace_dir in self.context.workspace_directories:
|
|
70
|
+
result = await self._try_resolve_in_workspace(path_name, workspace_dir, on_debug_message)
|
|
71
|
+
if result.resolved_path:
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
# If not found, return not_found result
|
|
75
|
+
on_debug_message(f'Path {path_name} not found in any workspace directory')
|
|
76
|
+
return PathResolutionResult(
|
|
77
|
+
resolved_path=None,
|
|
78
|
+
original_path=at_path,
|
|
79
|
+
resolution_type='not_found',
|
|
80
|
+
reason=f'Path not found in workspace directories: {self.context.workspace_directories}'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def _try_resolve_in_workspace(self, path_name: str, workspace_dir: str,
|
|
84
|
+
on_debug_message: callable) -> PathResolutionResult:
|
|
85
|
+
"""
|
|
86
|
+
Try to resolve path within a specific workspace directory
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path_name: Path name without '@' prefix
|
|
90
|
+
workspace_dir: Workspace directory to search in
|
|
91
|
+
on_debug_message: Debug message callback
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
PathResolutionResult
|
|
95
|
+
"""
|
|
96
|
+
workspace_path = Path(workspace_dir).resolve()
|
|
97
|
+
|
|
98
|
+
# Check if workspace directory is within target directory
|
|
99
|
+
if not self._is_workspace_within_target(workspace_path):
|
|
100
|
+
on_debug_message(f'Workspace {workspace_dir} is not within target directory {self.target_dir}')
|
|
101
|
+
return PathResolutionResult(
|
|
102
|
+
resolved_path=None,
|
|
103
|
+
original_path=f"@{path_name}",
|
|
104
|
+
resolution_type='not_found',
|
|
105
|
+
reason='Workspace not within target directory'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Try direct path resolution
|
|
109
|
+
direct_result = self._try_direct_resolution(path_name, workspace_path, on_debug_message)
|
|
110
|
+
if direct_result.resolved_path:
|
|
111
|
+
return direct_result
|
|
112
|
+
|
|
113
|
+
# Try glob search if enabled
|
|
114
|
+
if self.context.enable_recursive_search:
|
|
115
|
+
glob_result = await self._try_glob_search(path_name, workspace_path, on_debug_message)
|
|
116
|
+
if glob_result.resolved_path:
|
|
117
|
+
return glob_result
|
|
118
|
+
|
|
119
|
+
return PathResolutionResult(
|
|
120
|
+
resolved_path=None,
|
|
121
|
+
original_path=f"@{path_name}",
|
|
122
|
+
resolution_type='not_found',
|
|
123
|
+
reason=f'Not found in workspace: {workspace_dir}'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _try_direct_resolution(self, path_name: str, workspace_path: Path,
|
|
127
|
+
on_debug_message: callable) -> PathResolutionResult:
|
|
128
|
+
"""
|
|
129
|
+
Try direct path resolution (exact match)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
path_name: Path name to resolve
|
|
133
|
+
workspace_path: Workspace directory path
|
|
134
|
+
on_debug_message: Debug message callback
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
PathResolutionResult
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
absolute_path = workspace_path / path_name
|
|
141
|
+
resolved_path = absolute_path.resolve()
|
|
142
|
+
|
|
143
|
+
# Security check - ensure resolved path is within workspace
|
|
144
|
+
if not self._is_path_within_workspace(resolved_path, workspace_path):
|
|
145
|
+
raise WorkspaceSecurityError(f"Resolved path is outside workspace: {resolved_path}")
|
|
146
|
+
|
|
147
|
+
if resolved_path.exists():
|
|
148
|
+
if resolved_path.is_dir():
|
|
149
|
+
# Directory - convert to glob pattern
|
|
150
|
+
glob_pattern = path_name + ('**' if path_name.endswith('/') else '/**')
|
|
151
|
+
on_debug_message(f'Path {path_name} resolved to directory, using glob: {glob_pattern}')
|
|
152
|
+
return PathResolutionResult(
|
|
153
|
+
resolved_path=glob_pattern,
|
|
154
|
+
original_path=f"@{path_name}",
|
|
155
|
+
resolution_type='directory',
|
|
156
|
+
reason=f'Directory converted to glob pattern: {glob_pattern}'
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
# File - return relative path
|
|
160
|
+
relative_path = os.path.relpath(str(resolved_path), str(workspace_path))
|
|
161
|
+
on_debug_message(f'Path {path_name} resolved to file: {resolved_path}')
|
|
162
|
+
return PathResolutionResult(
|
|
163
|
+
resolved_path=relative_path,
|
|
164
|
+
original_path=f"@{path_name}",
|
|
165
|
+
resolution_type='direct',
|
|
166
|
+
reason=f'Direct file match: {resolved_path}'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
except (OSError, ValueError) as e:
|
|
170
|
+
on_debug_message(f'Error resolving path {path_name}: {e}')
|
|
171
|
+
|
|
172
|
+
return PathResolutionResult(
|
|
173
|
+
resolved_path=None,
|
|
174
|
+
original_path=f"@{path_name}",
|
|
175
|
+
resolution_type='not_found',
|
|
176
|
+
reason='Direct resolution failed'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async def _try_glob_search(self, path_name: str, workspace_path: Path,
|
|
180
|
+
on_debug_message: callable) -> PathResolutionResult:
|
|
181
|
+
"""
|
|
182
|
+
Try glob search for fuzzy matching
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
path_name: Path name to search for
|
|
186
|
+
workspace_path: Workspace directory path
|
|
187
|
+
on_debug_message: Debug message callback
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
PathResolutionResult
|
|
191
|
+
"""
|
|
192
|
+
on_debug_message(f'Path {path_name} not found directly, attempting glob search')
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Create glob pattern for fuzzy search
|
|
196
|
+
glob_pattern = f"**/*{path_name}*"
|
|
197
|
+
search_path = workspace_path / glob_pattern
|
|
198
|
+
|
|
199
|
+
# Use glob to find matching files
|
|
200
|
+
matches = list(workspace_path.glob(glob_pattern))
|
|
201
|
+
|
|
202
|
+
if matches:
|
|
203
|
+
# Take the first match
|
|
204
|
+
first_match = matches[0].resolve()
|
|
205
|
+
|
|
206
|
+
# Security check
|
|
207
|
+
if not self._is_path_within_workspace(first_match, workspace_path):
|
|
208
|
+
raise WorkspaceSecurityError(f"Glob match is outside workspace: {first_match}")
|
|
209
|
+
|
|
210
|
+
relative_path = os.path.relpath(str(first_match), str(workspace_path))
|
|
211
|
+
on_debug_message(f'Glob search for {path_name} found {first_match}, using relative path: {relative_path}')
|
|
212
|
+
|
|
213
|
+
return PathResolutionResult(
|
|
214
|
+
resolved_path=relative_path,
|
|
215
|
+
original_path=f"@{path_name}",
|
|
216
|
+
resolution_type='glob',
|
|
217
|
+
reason=f'Glob search found: {first_match} (first of {len(matches)} matches)'
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
on_debug_message(f'Glob search for "**/*{path_name}*" found no files')
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
on_debug_message(f'Error during glob search for {path_name}: {e}')
|
|
224
|
+
|
|
225
|
+
return PathResolutionResult(
|
|
226
|
+
resolved_path=None,
|
|
227
|
+
original_path=f"@{path_name}",
|
|
228
|
+
resolution_type='not_found',
|
|
229
|
+
reason='Glob search found no matches'
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def _is_path_safe(self, path_name: str) -> bool:
|
|
233
|
+
"""
|
|
234
|
+
Check if path is safe (no path traversal attempts)
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
path_name: Path to check
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
True if path is safe, False otherwise
|
|
241
|
+
"""
|
|
242
|
+
# Check for path traversal patterns
|
|
243
|
+
dangerous_patterns = ['../', '..\\', '../', '..\\']
|
|
244
|
+
for pattern in dangerous_patterns:
|
|
245
|
+
if pattern in path_name:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
# Check for absolute paths
|
|
249
|
+
if os.path.isabs(path_name):
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
# Check for null bytes
|
|
253
|
+
if '\x00' in path_name:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
def _is_workspace_within_target(self, workspace_path: Path) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Check if workspace directory is within target directory
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
workspace_path: Workspace directory path
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if workspace is within target, False otherwise
|
|
267
|
+
"""
|
|
268
|
+
try:
|
|
269
|
+
workspace_path.relative_to(self.target_dir)
|
|
270
|
+
return True
|
|
271
|
+
except ValueError:
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
def _is_path_within_workspace(self, file_path: Path, workspace_path: Path) -> bool:
|
|
275
|
+
"""
|
|
276
|
+
Check if file path is within workspace directory
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
file_path: File path to check
|
|
280
|
+
workspace_path: Workspace directory path
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if file is within workspace, False otherwise
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
file_path.relative_to(workspace_path)
|
|
287
|
+
return True
|
|
288
|
+
except ValueError:
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
def should_ignore_file(self, file_path: str) -> Tuple[bool, Optional[str]]:
|
|
292
|
+
"""
|
|
293
|
+
Check if file should be ignored based on filtering options
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
file_path: File path to check
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Tuple of (should_ignore, reason)
|
|
300
|
+
"""
|
|
301
|
+
# This is a simplified implementation
|
|
302
|
+
# In a full implementation, this would integrate with .gitignore parsing
|
|
303
|
+
# For now, we'll use basic patterns
|
|
304
|
+
|
|
305
|
+
ignore_patterns = [
|
|
306
|
+
'__pycache__',
|
|
307
|
+
'.git',
|
|
308
|
+
'.vscode',
|
|
309
|
+
'.idea',
|
|
310
|
+
'node_modules',
|
|
311
|
+
'.env'
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
for pattern in ignore_patterns:
|
|
315
|
+
if pattern in file_path:
|
|
316
|
+
return True, f"Matches ignore pattern: {pattern}"
|
|
317
|
+
|
|
318
|
+
return False, None
|