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,247 @@
1
+ # HandleAtCommand Service
2
+
3
+ A comprehensive service for processing @ commands in user queries, enabling intelligent file content injection into AI context.
4
+
5
+ ## Overview
6
+
7
+ The HandleAtCommand service allows users to reference files using simple `@` syntax in their queries. The service automatically:
8
+
9
+ 1. **Parses** user input to extract @ commands
10
+ 2. **Resolves** file paths using intelligent matching
11
+ 3. **Reads** file contents using the existing ReadManyFiles tool
12
+ 4. **Injects** content into the AI context seamlessly
13
+
14
+ ## Features
15
+
16
+ ### 🎯 Core Functionality
17
+ - **Smart Path Resolution**: Direct matching, fuzzy search, directory expansion
18
+ - **Multiple File Support**: Handle multiple @ commands in a single query
19
+ - **Content Injection**: Automatic file content integration into AI context
20
+ - **Error Handling**: Graceful handling of missing or inaccessible files
21
+
22
+ ### 🔍 Supported @ Command Formats
23
+ - `@filename.ext` - Single file reference
24
+ - `@path/to/file.ext` - Relative path file reference
25
+ - `@directory/` - Directory reference (converted to glob pattern)
26
+ - `@my\ file.txt` - Files with spaces (escaped)
27
+ - `请解释 @file1.js 和 @file2.py` - Mixed text and file references
28
+
29
+ ### 🛡️ Security Features
30
+ - **Workspace Boundary Checking**: Ensures files are within allowed directories
31
+ - **Path Traversal Protection**: Prevents `../` attacks
32
+ - **Git Ignore Support**: Respects .gitignore rules
33
+ - **File Size Limits**: Prevents reading of excessively large files
34
+
35
+ ## Architecture
36
+
37
+ ```
38
+ HandleAtCommand/
39
+ ├── processor.py # Main coordinator (AtCommandProcessor)
40
+ ├── parser.py # @ command parsing (AtCommandParser)
41
+ ├── resolver.py # Path resolution (PathResolver)
42
+ ├── models.py # Data structures
43
+ ├── exceptions.py # Custom exceptions
44
+ ├── utils.py # Utility functions
45
+ └── __init__.py # Package interface
46
+ ```
47
+
48
+ ### Component Responsibilities
49
+
50
+ #### AtCommandProcessor
51
+ - **Main Entry Point**: Coordinates all components
52
+ - **File Reading**: Integrates with ReadManyFilesTool
53
+ - **Query Reconstruction**: Builds final processed query
54
+ - **Statistics Tracking**: Monitors processing metrics
55
+
56
+ #### AtCommandParser
57
+ - **Input Parsing**: Extracts @ commands from user queries
58
+ - **Escape Handling**: Processes escaped characters in paths
59
+ - **Content Extraction**: Parses file content from tool output
60
+
61
+ #### PathResolver
62
+ - **Path Resolution**: Converts @ paths to actual file paths
63
+ - **Security Validation**: Ensures paths are safe and within workspace
64
+ - **Fuzzy Matching**: Provides glob-based search for partial matches
65
+ - **Directory Handling**: Converts directories to appropriate glob patterns
66
+
67
+ ## Usage
68
+
69
+ ### Basic Usage
70
+
71
+ ```python
72
+ from siada.services.handle_at_command import AtCommandProcessor, HandleAtCommandParams
73
+
74
+ # Initialize processor
75
+ processor = AtCommandProcessor(config)
76
+
77
+ # Create parameters
78
+ params = HandleAtCommandParams(
79
+ query="Please explain @main.py and @config.json",
80
+ config=config,
81
+ add_item=add_item_function,
82
+ on_debug_message=debug_function,
83
+ message_id=123
84
+ )
85
+
86
+ # Process @ commands
87
+ result = await processor.handle_at_command(params)
88
+
89
+ if result.should_proceed:
90
+ # Use result.processed_query for AI context
91
+ print("Processed query:", result.processed_query)
92
+ ```
93
+
94
+ ### Convenience Function
95
+
96
+ ```python
97
+ from siada.services.handle_at_command import handle_at_command
98
+
99
+ result = await handle_at_command(
100
+ query="Show me @file.py",
101
+ config=config,
102
+ add_item=add_item_function,
103
+ on_debug_message=debug_function,
104
+ message_id=123
105
+ )
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ ### Required Configuration Properties
111
+
112
+ ```python
113
+ class Config:
114
+ root_dir: str # Root directory for file operations
115
+
116
+ # Optional: Additional workspace directories
117
+ # Optional: File filtering options
118
+ # Optional: Enable recursive search
119
+ ```
120
+
121
+ ### File Filtering Options
122
+
123
+ ```python
124
+ file_filtering_options = {
125
+ 'respect_git_ignore': True, # Respect .gitignore rules
126
+ # Add more filtering options as needed
127
+ }
128
+ ```
129
+
130
+ ## Examples
131
+
132
+ ### Single File Reference
133
+ ```
134
+ Input: "Explain @main.py"
135
+ Output: Query with main.py content injected
136
+ ```
137
+
138
+ ### Multiple Files
139
+ ```
140
+ Input: "Compare @file1.py and @file2.py"
141
+ Output: Query with both files' content injected
142
+ ```
143
+
144
+ ### Directory Reference
145
+ ```
146
+ Input: "Show me @src/"
147
+ Output: Query with all files in src/ directory
148
+ ```
149
+
150
+ ### Mixed Content
151
+ ```
152
+ Input: "The function in @utils.py needs to handle @config.json"
153
+ Output: Query with both files' content injected
154
+ ```
155
+
156
+ ### Fuzzy Matching
157
+ ```
158
+ Input: "Check @main"
159
+ Output: Finds and includes main.py (if it exists)
160
+ ```
161
+
162
+ ## Error Handling
163
+
164
+ The service handles various error conditions gracefully:
165
+
166
+ - **File Not Found**: Attempts fuzzy search, reports if not found
167
+ - **Permission Denied**: Skips file, continues with others
168
+ - **Path Outside Workspace**: Security violation, file skipped
169
+ - **File Too Large**: Respects size limits, skips oversized files
170
+
171
+ ## Testing
172
+
173
+ ### Running Tests
174
+
175
+ ```bash
176
+ # Run parser tests
177
+ cd tests/services/handle_at_command
178
+ python test_parser.py
179
+
180
+ # Run demo
181
+ python demo_handle_at_command.py
182
+ ```
183
+
184
+ ### Test Coverage
185
+
186
+ - ✅ @ Command parsing (various formats)
187
+ - ✅ Path resolution (direct, fuzzy, directory)
188
+ - ✅ File content injection
189
+ - ✅ Error handling
190
+ - ✅ Security validation
191
+ - ✅ Integration with ReadManyFilesTool
192
+
193
+ ## Performance Considerations
194
+
195
+ ### Optimization Features
196
+ - **Concurrent Processing**: Multiple files processed in parallel
197
+ - **Intelligent Caching**: Avoids re-reading unchanged files
198
+ - **Content Limits**: Respects file size and line count limits
199
+ - **Early Exit**: Skips processing if no @ commands found
200
+
201
+ ### Performance Metrics
202
+ - Processing time tracking
203
+ - File read statistics
204
+ - Path resolution success rates
205
+ - Error categorization
206
+
207
+ ## Integration Points
208
+
209
+ ### Dependencies
210
+ - **ReadManyFilesTool**: For actual file reading
211
+ - **FileFilter**: For .gitignore and filtering support
212
+ - **PathLib**: For cross-platform path handling
213
+
214
+ ### Extension Points
215
+ - Custom path resolvers
216
+ - Additional file type support
217
+ - Enhanced security policies
218
+ - Custom content formatters
219
+
220
+ ## Future Enhancements
221
+
222
+ ### Planned Features
223
+ - **Caching Layer**: Persistent file content caching
224
+ - **Advanced Filtering**: More sophisticated ignore rules
225
+ - **Content Preprocessing**: Syntax highlighting, formatting
226
+ - **Batch Operations**: Optimized multi-file processing
227
+ - **Plugin System**: Extensible resolver and formatter plugins
228
+
229
+ ### Potential Improvements
230
+ - **AI-Powered Matching**: Semantic file matching
231
+ - **Context-Aware Resolution**: Smart path suggestions
232
+ - **Real-time Updates**: File change monitoring
233
+ - **Performance Analytics**: Detailed performance insights
234
+
235
+ ## Contributing
236
+
237
+ When contributing to this service:
238
+
239
+ 1. **Follow Architecture**: Maintain separation of concerns
240
+ 2. **Add Tests**: Include unit tests for new functionality
241
+ 3. **Update Documentation**: Keep README and docstrings current
242
+ 4. **Security First**: Consider security implications of changes
243
+ 5. **Performance Aware**: Monitor impact on processing speed
244
+
245
+ ## License
246
+
247
+ This service is part of the Siada project and follows the project's licensing terms.
@@ -0,0 +1,115 @@
1
+ """
2
+ HandleAtCommand - A service for processing @ commands in user queries.
3
+
4
+ This package provides functionality to parse user input containing @ commands,
5
+ resolve file paths, read file contents, and inject them into AI context.
6
+
7
+ Main Components:
8
+ - AtCommandProcessor: Main processor that coordinates all components
9
+ - AtCommandParser: Parses @ commands from user input
10
+ - PathResolver: Resolves @ paths to actual file paths
11
+ - Models: Data structures and type definitions
12
+ - Exceptions: Custom exception classes
13
+ - Utils: Utility functions
14
+
15
+ Usage:
16
+ from siada.services.handle_at_command import AtCommandProcessor, HandleAtCommandParams
17
+
18
+ processor = AtCommandProcessor(config)
19
+ params = HandleAtCommandParams(
20
+ query="Please explain @file.py",
21
+ config=config,
22
+ add_item=add_item_func,
23
+ on_debug_message=debug_func,
24
+ message_id=123
25
+ )
26
+ result = await processor.handle_at_command(params)
27
+ """
28
+
29
+ from .processor import AtCommandProcessor
30
+ from .parser import AtCommandParser
31
+ from .resolver import PathResolver
32
+ from .models import (
33
+ AtCommandPart,
34
+ HandleAtCommandParams,
35
+ HandleAtCommandResult,
36
+ ResolverContext,
37
+ PathResolutionResult,
38
+ ProcessingStats,
39
+ IgnoredFileStats
40
+ )
41
+ from .exceptions import (
42
+ AtCommandError,
43
+ PathNotFoundError,
44
+ PathIgnoredError,
45
+ FileTooLargeError,
46
+ PermissionDeniedError,
47
+ SecurityViolationError,
48
+ InvalidPathError,
49
+ WorkspaceSecurityError
50
+ )
51
+ from . import utils
52
+
53
+ __all__ = [
54
+ # Main classes
55
+ 'AtCommandProcessor',
56
+ 'AtCommandParser',
57
+ 'PathResolver',
58
+
59
+ # Data models
60
+ 'AtCommandPart',
61
+ 'HandleAtCommandParams',
62
+ 'HandleAtCommandResult',
63
+ 'ResolverContext',
64
+ 'PathResolutionResult',
65
+ 'ProcessingStats',
66
+ 'IgnoredFileStats',
67
+
68
+ # Exceptions
69
+ 'AtCommandError',
70
+ 'PathNotFoundError',
71
+ 'PathIgnoredError',
72
+ 'FileTooLargeError',
73
+ 'PermissionDeniedError',
74
+ 'SecurityViolationError',
75
+ 'InvalidPathError',
76
+ 'WorkspaceSecurityError',
77
+
78
+ # Utilities
79
+ 'utils'
80
+ ]
81
+
82
+ # Version information
83
+ __version__ = '1.0.0'
84
+ __author__ = 'Siada Team'
85
+ __description__ = 'A service for processing @ commands in user queries'
86
+
87
+
88
+ # Convenience function for easy usage
89
+ async def handle_at_command(query: str, config, add_item: callable,
90
+ on_debug_message: callable, message_id: int,
91
+ signal=None) -> HandleAtCommandResult:
92
+ """
93
+ Convenience function to handle @ commands
94
+
95
+ Args:
96
+ query: User input query
97
+ config: Configuration object
98
+ add_item: Function to add items to history
99
+ on_debug_message: Debug message callback
100
+ message_id: Message ID
101
+ signal: Cancellation signal (optional)
102
+
103
+ Returns:
104
+ HandleAtCommandResult with processed query and status
105
+ """
106
+ processor = AtCommandProcessor(config)
107
+ params = HandleAtCommandParams(
108
+ query=query,
109
+ config=config,
110
+ add_item=add_item,
111
+ on_debug_message=on_debug_message,
112
+ message_id=message_id,
113
+ signal=signal
114
+ )
115
+ return await processor.handle_at_command(params)
@@ -0,0 +1,46 @@
1
+ """
2
+ Custom exceptions for HandleAtCommand functionality.
3
+ """
4
+
5
+
6
+ class AtCommandError(Exception):
7
+ """Base class for @ command processing errors"""
8
+
9
+ def __init__(self, message: str, file_path: str = None):
10
+ super().__init__(message)
11
+ self.file_path = file_path
12
+
13
+
14
+ class PathNotFoundError(AtCommandError):
15
+ """Raised when a path cannot be found"""
16
+ pass
17
+
18
+
19
+ class PathIgnoredError(AtCommandError):
20
+ """Raised when a path is ignored by filtering rules"""
21
+ pass
22
+
23
+
24
+ class FileTooLargeError(AtCommandError):
25
+ """Raised when a file exceeds size limits"""
26
+ pass
27
+
28
+
29
+ class PermissionDeniedError(AtCommandError):
30
+ """Raised when access to a file is denied"""
31
+ pass
32
+
33
+
34
+ class SecurityViolationError(AtCommandError):
35
+ """Raised when a security violation is detected (e.g., path traversal)"""
36
+ pass
37
+
38
+
39
+ class InvalidPathError(AtCommandError):
40
+ """Raised when a path is invalid or malformed"""
41
+ pass
42
+
43
+
44
+ class WorkspaceSecurityError(AtCommandError):
45
+ """Raised when a path is outside the workspace boundaries"""
46
+ pass
@@ -0,0 +1,92 @@
1
+ """
2
+ HandleAtCommand data models and structures.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import List, Dict, Optional, Union, Any, Literal
7
+
8
+
9
+ @dataclass
10
+ class AtCommandPart:
11
+ """Represents a part of the parsed @ command query"""
12
+ type: Literal['text', 'atPath'] # Part type
13
+ content: str # Content
14
+
15
+
16
+ @dataclass
17
+ class HandleAtCommandParams:
18
+ """Input parameters for handleAtCommand function"""
19
+ query: str # User's original query
20
+ config: Any # Configuration object
21
+ add_item: callable # History addition function
22
+ on_debug_message: callable # Debug message callback
23
+ message_id: int # Message ID
24
+ signal: Optional[Any] = None # Cancellation signal
25
+
26
+
27
+ @dataclass
28
+ class HandleAtCommandResult:
29
+ """Output result from handleAtCommand function"""
30
+ processed_query: Optional[List[Dict]] # Processed query parts
31
+ should_proceed: bool # Whether to continue processing
32
+
33
+
34
+ @dataclass
35
+ class ResolverContext:
36
+ """Context for path resolution"""
37
+ workspace_directories: List[str]
38
+ target_directory: str
39
+ enable_recursive_search: bool = True
40
+ file_filtering_options: Optional[Dict[str, bool]] = None
41
+
42
+ def __post_init__(self):
43
+ if self.file_filtering_options is None:
44
+ self.file_filtering_options = {
45
+ 'respect_git_ignore': True
46
+ }
47
+
48
+
49
+ @dataclass
50
+ class PathResolutionResult:
51
+ """Result of path resolution"""
52
+ resolved_path: Optional[str]
53
+ original_path: str
54
+ resolution_type: Literal['direct', 'glob', 'directory', 'not_found']
55
+ reason: Optional[str] = None
56
+
57
+
58
+ @dataclass
59
+ class ProcessingStats:
60
+ """Statistics for @ command processing"""
61
+ total_at_commands: int = 0
62
+ resolved_paths: int = 0
63
+ failed_paths: int = 0
64
+ ignored_paths: int = 0
65
+ files_read: int = 0
66
+ processing_time: float = 0.0
67
+
68
+
69
+ @dataclass
70
+ class IgnoredFileStats:
71
+ """Statistics for ignored files"""
72
+ git_ignored: List[str]
73
+ gemini_ignored: List[str]
74
+ both_ignored: List[str]
75
+
76
+ def __init__(self):
77
+ self.git_ignored = []
78
+ self.gemini_ignored = []
79
+ self.both_ignored = []
80
+
81
+ def add_ignored_file(self, file_path: str, reason: Literal['git', 'gemini', 'both']):
82
+ """Add a file to the ignored list"""
83
+ if reason == 'git':
84
+ self.git_ignored.append(file_path)
85
+ elif reason == 'gemini':
86
+ self.gemini_ignored.append(file_path)
87
+ elif reason == 'both':
88
+ self.both_ignored.append(file_path)
89
+
90
+ def get_total_ignored(self) -> int:
91
+ """Get total number of ignored files"""
92
+ return len(self.git_ignored) + len(self.gemini_ignored) + len(self.both_ignored)
@@ -0,0 +1,189 @@
1
+ """
2
+ @ Command Parser - Parses user input to extract @ commands.
3
+ """
4
+
5
+ import re
6
+ from typing import List
7
+
8
+ from .models import AtCommandPart
9
+ from .exceptions import InvalidPathError
10
+
11
+
12
+ class AtCommandParser:
13
+ """Parser for @ commands in user queries"""
14
+
15
+ def __init__(self):
16
+ # Regex pattern to match file content format from read_many_files
17
+ self.file_content_regex = re.compile(r'^--- (.*?) ---\n\n([\s\S]*?)\n\n$')
18
+
19
+ def parse_all_at_commands(self, query: str) -> List[AtCommandPart]:
20
+ """
21
+ Parse all @ commands from the query string
22
+
23
+ Args:
24
+ query: User input query string
25
+
26
+ Returns:
27
+ List of AtCommandPart objects representing parsed parts
28
+ """
29
+ if not query:
30
+ return []
31
+
32
+ parts = []
33
+ current_index = 0
34
+
35
+ while current_index < len(query):
36
+ # Find next unescaped '@'
37
+ at_index = self._find_next_unescaped_at(query, current_index)
38
+
39
+ if at_index == -1:
40
+ # No more '@' symbols, add remaining text
41
+ if current_index < len(query):
42
+ remaining_text = query[current_index:]
43
+ if remaining_text.strip(): # Only add non-empty text
44
+ parts.append(AtCommandPart('text', remaining_text))
45
+ break
46
+
47
+ # Add text before '@' if any
48
+ if at_index > current_index:
49
+ text_before = query[current_index:at_index]
50
+ if text_before.strip(): # Only add non-empty text
51
+ parts.append(AtCommandPart('text', text_before))
52
+
53
+ # Parse '@path'
54
+ path_end_index = self._find_path_end(query, at_index + 1)
55
+ raw_at_path = query[at_index:path_end_index]
56
+
57
+ # Handle lone '@' symbol
58
+ if raw_at_path == '@':
59
+ parts.append(AtCommandPart('text', '@'))
60
+ else:
61
+ at_path = self._unescape_path(raw_at_path)
62
+ parts.append(AtCommandPart('atPath', at_path))
63
+
64
+ current_index = path_end_index
65
+
66
+ # Filter out empty text parts
67
+ return [p for p in parts if not (p.type == 'text' and not p.content.strip())]
68
+
69
+ def _find_next_unescaped_at(self, query: str, start_index: int) -> int:
70
+ """
71
+ Find the next unescaped '@' symbol
72
+
73
+ Args:
74
+ query: Query string
75
+ start_index: Starting index for search
76
+
77
+ Returns:
78
+ Index of next unescaped '@', or -1 if not found
79
+ """
80
+ index = start_index
81
+ while index < len(query):
82
+ if query[index] == '@':
83
+ # Check if it's escaped (preceded by backslash)
84
+ if index == 0 or query[index - 1] != '\\':
85
+ return index
86
+ index += 1
87
+ return -1
88
+
89
+ def _find_path_end(self, query: str, start_index: int) -> int:
90
+ """
91
+ Find the end of the path, handling escape characters
92
+
93
+ Args:
94
+ query: Query string
95
+ start_index: Starting index (after '@')
96
+
97
+ Returns:
98
+ Index where the path ends
99
+ """
100
+ index = start_index
101
+ in_escape = False
102
+
103
+ while index < len(query):
104
+ char = query[index]
105
+
106
+ if in_escape:
107
+ # Previous character was escape, skip this character
108
+ in_escape = False
109
+ elif char == '\\':
110
+ # This is an escape character
111
+ in_escape = True
112
+ elif char.isspace():
113
+ # Unescaped whitespace marks end of path
114
+ break
115
+
116
+ index += 1
117
+
118
+ return index
119
+
120
+ def _unescape_path(self, path: str) -> str:
121
+ """
122
+ Process escape characters in the path
123
+
124
+ Args:
125
+ path: Raw path string with potential escape characters
126
+
127
+ Returns:
128
+ Unescaped path string
129
+ """
130
+ if not path:
131
+ return path
132
+
133
+ # Handle escaped spaces and other characters
134
+ result = []
135
+ i = 0
136
+ while i < len(path):
137
+ if path[i] == '\\' and i + 1 < len(path):
138
+ # Escape sequence - add the next character literally
139
+ result.append(path[i + 1])
140
+ i += 2
141
+ else:
142
+ result.append(path[i])
143
+ i += 1
144
+
145
+ return ''.join(result)
146
+
147
+ def validate_at_path(self, at_path: str) -> bool:
148
+ """
149
+ Validate that an @ path is well-formed
150
+
151
+ Args:
152
+ at_path: @ path string (including '@' prefix)
153
+
154
+ Returns:
155
+ True if valid, False otherwise
156
+ """
157
+ if not at_path or not at_path.startswith('@'):
158
+ return False
159
+
160
+ path_part = at_path[1:] # Remove '@' prefix
161
+
162
+ # Empty path after '@' is invalid (except lone '@')
163
+ if not path_part and at_path != '@':
164
+ return False
165
+
166
+ # Check for invalid characters
167
+ invalid_chars = ['<', '>', '|', '"', '*', '?']
168
+ for char in invalid_chars:
169
+ if char in path_part:
170
+ return False
171
+
172
+ return True
173
+
174
+ def extract_file_content_info(self, content_part: str) -> tuple[str, str]:
175
+ """
176
+ Extract file path and content from formatted file content
177
+
178
+ Args:
179
+ content_part: Formatted content from read_many_files tool
180
+
181
+ Returns:
182
+ Tuple of (file_path, content) or (None, content) if not matched
183
+ """
184
+ match = self.file_content_regex.match(content_part)
185
+ if match:
186
+ file_path = match.group(1)
187
+ content = match.group(2).strip()
188
+ return file_path, content
189
+ return None, content_part