kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Command menu renderer for interactive slash command display."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
from ..events.models import CommandCategory
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CommandMenuRenderer:
|
|
11
|
+
"""Renders interactive command menu overlay.
|
|
12
|
+
|
|
13
|
+
Provides a command menu that appears when the user
|
|
14
|
+
types '/' and allows filtering and selection of available commands.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, terminal_renderer) -> None:
|
|
18
|
+
"""Initialize the command menu renderer.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
terminal_renderer: Terminal renderer for display operations.
|
|
22
|
+
"""
|
|
23
|
+
self.renderer = terminal_renderer
|
|
24
|
+
self.logger = logger
|
|
25
|
+
self.menu_active = False
|
|
26
|
+
self.current_commands = []
|
|
27
|
+
self.selected_index = 0
|
|
28
|
+
self.filter_text = ""
|
|
29
|
+
self.current_menu_lines = [] # Store menu content for event system
|
|
30
|
+
|
|
31
|
+
def show_command_menu(self, commands: List[Dict[str, Any]], filter_text: str = "") -> None:
|
|
32
|
+
"""Display command menu when user types '/'.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
commands: List of available commands to display.
|
|
36
|
+
filter_text: Current filter text (excluding the leading '/').
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
self.menu_active = True
|
|
40
|
+
self.current_commands = commands
|
|
41
|
+
self.filter_text = filter_text
|
|
42
|
+
self.selected_index = 0
|
|
43
|
+
|
|
44
|
+
# Render the menu
|
|
45
|
+
self._render_menu()
|
|
46
|
+
|
|
47
|
+
self.logger.info(f"Command menu shown with {len(commands)} commands")
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.logger.error(f"Error showing command menu: {e}")
|
|
51
|
+
|
|
52
|
+
def set_selected_index(self, index: int) -> None:
|
|
53
|
+
"""Set the selected command index for navigation.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
index: Index of the command to select.
|
|
57
|
+
"""
|
|
58
|
+
if 0 <= index < len(self.current_commands):
|
|
59
|
+
self.selected_index = index
|
|
60
|
+
# Note: No auto-render here - caller will trigger render to avoid duplicates
|
|
61
|
+
logger.debug(f"Selected command index set to: {index}")
|
|
62
|
+
|
|
63
|
+
def hide_menu(self) -> None:
|
|
64
|
+
"""Hide command menu and return to normal input."""
|
|
65
|
+
try:
|
|
66
|
+
if self.menu_active:
|
|
67
|
+
self.menu_active = False
|
|
68
|
+
self.current_commands = []
|
|
69
|
+
self.selected_index = 0
|
|
70
|
+
self.filter_text = ""
|
|
71
|
+
|
|
72
|
+
# Clear menu from display
|
|
73
|
+
self._clear_menu()
|
|
74
|
+
|
|
75
|
+
self.logger.info("Command menu hidden")
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self.logger.error(f"Error hiding command menu: {e}")
|
|
79
|
+
|
|
80
|
+
def filter_commands(self, commands: List[Dict[str, Any]], filter_text: str, reset_selection: bool = True) -> None:
|
|
81
|
+
"""Filter visible commands as user types.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
commands: Filtered list of commands to display.
|
|
85
|
+
filter_text: Current filter text.
|
|
86
|
+
reset_selection: Whether to reset selection to top (True for typing, False for navigation).
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
if not self.menu_active:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
self.current_commands = commands
|
|
93
|
+
self.filter_text = filter_text
|
|
94
|
+
|
|
95
|
+
# Only reset selection when filtering by typing, not during navigation
|
|
96
|
+
if reset_selection:
|
|
97
|
+
self.selected_index = 0 # Reset selection to top
|
|
98
|
+
else:
|
|
99
|
+
# Ensure selected index is still valid after filtering
|
|
100
|
+
if self.selected_index >= len(commands):
|
|
101
|
+
self.selected_index = max(0, len(commands) - 1)
|
|
102
|
+
|
|
103
|
+
# Re-render with filtered commands
|
|
104
|
+
self._render_menu()
|
|
105
|
+
|
|
106
|
+
self.logger.debug(f"Filtered to {len(commands)} commands with '{filter_text}', reset_selection={reset_selection}")
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
self.logger.error(f"Error filtering commands: {e}")
|
|
110
|
+
|
|
111
|
+
def navigate_selection(self, direction: str) -> bool:
|
|
112
|
+
"""Handle arrow key navigation in menu.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
direction: Direction to navigate ("up" or "down").
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if navigation was handled, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
if not self.menu_active or not self.current_commands:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
if direction == "up":
|
|
125
|
+
self.selected_index = max(0, self.selected_index - 1)
|
|
126
|
+
elif direction == "down":
|
|
127
|
+
self.selected_index = min(len(self.current_commands) - 1, self.selected_index + 1)
|
|
128
|
+
else:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Re-render with new selection
|
|
132
|
+
self._render_menu()
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
self.logger.error(f"Error navigating menu: {e}")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def get_selected_command(self) -> Optional[Dict[str, Any]]:
|
|
140
|
+
"""Get currently selected command.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Selected command dictionary or None if no selection.
|
|
144
|
+
"""
|
|
145
|
+
if (self.menu_active and
|
|
146
|
+
self.current_commands and
|
|
147
|
+
0 <= self.selected_index < len(self.current_commands)):
|
|
148
|
+
return self.current_commands[self.selected_index]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def _render_menu(self) -> None:
|
|
152
|
+
"""Render the command menu overlay."""
|
|
153
|
+
try:
|
|
154
|
+
if not self.menu_active:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Create menu content
|
|
158
|
+
menu_lines = self._create_menu_lines()
|
|
159
|
+
|
|
160
|
+
# Display menu overlay
|
|
161
|
+
self._display_menu_overlay(menu_lines)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
self.logger.error(f"Error rendering menu: {e}")
|
|
165
|
+
|
|
166
|
+
def _create_menu_lines(self) -> List[str]:
|
|
167
|
+
"""Create lines for menu display.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
List of formatted menu lines.
|
|
171
|
+
"""
|
|
172
|
+
lines = []
|
|
173
|
+
|
|
174
|
+
# Header (input display handled by enhanced input plugin)
|
|
175
|
+
# Note: Removed separator line as requested
|
|
176
|
+
|
|
177
|
+
# Group commands by category for organized display
|
|
178
|
+
categorized_commands = self._group_commands_by_category()
|
|
179
|
+
|
|
180
|
+
# Render each category
|
|
181
|
+
for category, commands in categorized_commands.items():
|
|
182
|
+
if not commands:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Category header (if multiple categories)
|
|
186
|
+
if len(categorized_commands) > 1:
|
|
187
|
+
category_name = self._format_category_name(category)
|
|
188
|
+
lines.append(f" {category_name}")
|
|
189
|
+
|
|
190
|
+
# Render commands in this category
|
|
191
|
+
for cmd in commands:
|
|
192
|
+
line = self._format_command_line(cmd)
|
|
193
|
+
lines.append(line)
|
|
194
|
+
|
|
195
|
+
# Add spacing between categories
|
|
196
|
+
if len(categorized_commands) > 1:
|
|
197
|
+
lines.append("")
|
|
198
|
+
|
|
199
|
+
# If no commands, show message
|
|
200
|
+
if not self.current_commands:
|
|
201
|
+
lines.append(" No matching commands found")
|
|
202
|
+
|
|
203
|
+
return lines
|
|
204
|
+
|
|
205
|
+
def _group_commands_by_category(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
206
|
+
"""Group commands by category for organized display.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dictionary mapping category names to command lists.
|
|
210
|
+
"""
|
|
211
|
+
categorized = {}
|
|
212
|
+
|
|
213
|
+
for i, cmd in enumerate(self.current_commands):
|
|
214
|
+
category = cmd.get("category", "custom")
|
|
215
|
+
if category not in categorized:
|
|
216
|
+
categorized[category] = []
|
|
217
|
+
|
|
218
|
+
# Add selection info to command
|
|
219
|
+
cmd_with_selection = cmd.copy()
|
|
220
|
+
cmd_with_selection["_is_selected"] = (i == self.selected_index)
|
|
221
|
+
cmd_with_selection["_index"] = i
|
|
222
|
+
|
|
223
|
+
categorized[category].append(cmd_with_selection)
|
|
224
|
+
|
|
225
|
+
return categorized
|
|
226
|
+
|
|
227
|
+
def _format_category_name(self, category: str) -> str:
|
|
228
|
+
"""Format category name for display.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
category: Category identifier.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Formatted category name.
|
|
235
|
+
"""
|
|
236
|
+
category_names = {
|
|
237
|
+
"system": "Core System",
|
|
238
|
+
"conversation": "Conversation Management",
|
|
239
|
+
"agent": "Agent Management",
|
|
240
|
+
"development": "Development Tools",
|
|
241
|
+
"file": "File Management",
|
|
242
|
+
"task": "Task Management",
|
|
243
|
+
"custom": "Plugin Commands"
|
|
244
|
+
}
|
|
245
|
+
return category_names.get(category, category.title())
|
|
246
|
+
|
|
247
|
+
def _format_command_line(self, cmd: Dict[str, Any]) -> str:
|
|
248
|
+
"""Format a single command line for display.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
cmd: Command dictionary with display info.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Formatted command line string.
|
|
255
|
+
"""
|
|
256
|
+
# Selection indicator
|
|
257
|
+
indicator = "❯ " if cmd.get("_is_selected", False) else " "
|
|
258
|
+
|
|
259
|
+
# Command name with aliases
|
|
260
|
+
name_part = f"/{cmd['name']}"
|
|
261
|
+
if cmd.get("aliases"):
|
|
262
|
+
aliases_str = ", ".join(cmd["aliases"])
|
|
263
|
+
name_part += f" ({aliases_str})"
|
|
264
|
+
|
|
265
|
+
# Description
|
|
266
|
+
description = cmd.get("description", "")
|
|
267
|
+
|
|
268
|
+
# Format the complete line
|
|
269
|
+
line = f"{indicator}{name_part:<30} {description}"
|
|
270
|
+
|
|
271
|
+
return line
|
|
272
|
+
|
|
273
|
+
def _display_menu_overlay(self, menu_lines: List[str]) -> None:
|
|
274
|
+
"""Display menu as overlay on terminal.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
menu_lines: Formatted menu lines to display.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
# Store menu content for INPUT_RENDER event response
|
|
281
|
+
self.current_menu_lines = menu_lines
|
|
282
|
+
|
|
283
|
+
# Log menu for debugging
|
|
284
|
+
self.logger.info("=== COMMAND MENU ===")
|
|
285
|
+
for line in menu_lines:
|
|
286
|
+
self.logger.info(line)
|
|
287
|
+
self.logger.info("=== END MENU ===")
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self.logger.error(f"Error preparing menu display: {e}")
|
|
291
|
+
|
|
292
|
+
def _clear_menu(self) -> None:
|
|
293
|
+
"""Clear menu from display."""
|
|
294
|
+
try:
|
|
295
|
+
# Clear overlay if renderer supports it
|
|
296
|
+
if hasattr(self.renderer, 'hide_overlay'):
|
|
297
|
+
self.renderer.hide_overlay()
|
|
298
|
+
elif hasattr(self.renderer, 'clear_menu'):
|
|
299
|
+
self.renderer.clear_menu()
|
|
300
|
+
else:
|
|
301
|
+
# Fallback: log clear
|
|
302
|
+
self.logger.info("Command menu cleared")
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
self.logger.error(f"Error clearing menu: {e}")
|
|
306
|
+
|
|
307
|
+
def get_menu_stats(self) -> Dict[str, Any]:
|
|
308
|
+
"""Get menu statistics for debugging.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Dictionary with menu statistics.
|
|
312
|
+
"""
|
|
313
|
+
return {
|
|
314
|
+
"active": self.menu_active,
|
|
315
|
+
"command_count": len(self.current_commands),
|
|
316
|
+
"selected_index": self.selected_index,
|
|
317
|
+
"filter_text": self.filter_text,
|
|
318
|
+
"selected_command": self.get_selected_command()
|
|
319
|
+
}
|
core/commands/parser.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Slash command parser for Kollabor CLI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import shlex
|
|
5
|
+
from typing import Optional, List
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ..events.models import SlashCommand
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SlashCommandParser:
|
|
14
|
+
"""Parses user input for slash commands.
|
|
15
|
+
|
|
16
|
+
Handles command detection, parsing, and argument extraction
|
|
17
|
+
with proper validation and error handling.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
"""Initialize the slash command parser."""
|
|
22
|
+
self.logger = logger
|
|
23
|
+
|
|
24
|
+
def is_slash_command(self, input_text: str) -> bool:
|
|
25
|
+
"""Check if input starts with a slash command.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
input_text: User input to check.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if input is a slash command, False otherwise.
|
|
32
|
+
"""
|
|
33
|
+
if not input_text:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Check if input starts with '/' and has content after
|
|
37
|
+
stripped = input_text.strip()
|
|
38
|
+
return stripped.startswith('/') and len(stripped) > 1
|
|
39
|
+
|
|
40
|
+
def parse_command(self, input_text: str) -> Optional[SlashCommand]:
|
|
41
|
+
"""Parse slash command from user input.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
input_text: Raw user input containing slash command.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Parsed SlashCommand object or None if parsing fails.
|
|
48
|
+
"""
|
|
49
|
+
if not self.is_slash_command(input_text):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Remove leading slash and strip whitespace
|
|
54
|
+
command_text = input_text.strip()[1:]
|
|
55
|
+
|
|
56
|
+
if not command_text:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
# Split command and arguments using shell-like parsing
|
|
60
|
+
# This handles quoted arguments properly: /save "my file.txt"
|
|
61
|
+
try:
|
|
62
|
+
parts = shlex.split(command_text)
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
# Handle malformed quotes gracefully
|
|
65
|
+
self.logger.warning(f"Quote parsing failed, using simple split: {e}")
|
|
66
|
+
parts = command_text.split()
|
|
67
|
+
|
|
68
|
+
if not parts:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
command_name = parts[0].lower()
|
|
72
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
73
|
+
|
|
74
|
+
# Create parsed command
|
|
75
|
+
slash_command = SlashCommand(
|
|
76
|
+
name=command_name,
|
|
77
|
+
args=args,
|
|
78
|
+
raw_input=input_text,
|
|
79
|
+
timestamp=datetime.now()
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Extract parameters for known parameter patterns
|
|
83
|
+
slash_command.parameters = self._extract_parameters(args)
|
|
84
|
+
|
|
85
|
+
self.logger.debug(f"Parsed command: {command_name} with {len(args)} args")
|
|
86
|
+
return slash_command
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
self.logger.error(f"Error parsing slash command '{input_text}': {e}")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def _extract_parameters(self, args: List[str]) -> dict:
|
|
93
|
+
"""Extract parameters from command arguments.
|
|
94
|
+
|
|
95
|
+
Supports patterns like:
|
|
96
|
+
- /config set theme dark
|
|
97
|
+
- /save --format json filename.json
|
|
98
|
+
- /load -f "my file.txt"
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
args: List of command arguments.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary of extracted parameters.
|
|
105
|
+
"""
|
|
106
|
+
parameters = {}
|
|
107
|
+
|
|
108
|
+
i = 0
|
|
109
|
+
while i < len(args):
|
|
110
|
+
arg = args[i]
|
|
111
|
+
|
|
112
|
+
# Handle --key=value format
|
|
113
|
+
if '=' in arg and arg.startswith('--'):
|
|
114
|
+
key, value = arg[2:].split('=', 1)
|
|
115
|
+
parameters[key] = value
|
|
116
|
+
|
|
117
|
+
# Handle --key value format
|
|
118
|
+
elif arg.startswith('--') and i + 1 < len(args):
|
|
119
|
+
key = arg[2:]
|
|
120
|
+
value = args[i + 1]
|
|
121
|
+
parameters[key] = value
|
|
122
|
+
i += 1 # Skip next arg since we consumed it
|
|
123
|
+
|
|
124
|
+
# Handle -k value format (short flags)
|
|
125
|
+
elif arg.startswith('-') and len(arg) == 2 and i + 1 < len(args):
|
|
126
|
+
key = arg[1:]
|
|
127
|
+
value = args[i + 1]
|
|
128
|
+
parameters[key] = value
|
|
129
|
+
i += 1 # Skip next arg since we consumed it
|
|
130
|
+
|
|
131
|
+
# Handle boolean flags
|
|
132
|
+
elif arg.startswith('--'):
|
|
133
|
+
key = arg[2:]
|
|
134
|
+
parameters[key] = True
|
|
135
|
+
|
|
136
|
+
elif arg.startswith('-') and len(arg) == 2:
|
|
137
|
+
key = arg[1:]
|
|
138
|
+
parameters[key] = True
|
|
139
|
+
|
|
140
|
+
i += 1
|
|
141
|
+
|
|
142
|
+
return parameters
|
|
143
|
+
|
|
144
|
+
def validate_command(self, command: SlashCommand) -> List[str]:
|
|
145
|
+
"""Validate a parsed command for basic correctness.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
command: Parsed slash command to validate.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of validation errors, empty if valid.
|
|
152
|
+
"""
|
|
153
|
+
errors = []
|
|
154
|
+
|
|
155
|
+
# Validate command name
|
|
156
|
+
if not command.name:
|
|
157
|
+
errors.append("Command name cannot be empty")
|
|
158
|
+
elif not command.name.isalnum() and '-' not in command.name and '_' not in command.name:
|
|
159
|
+
errors.append(f"Invalid command name: {command.name}")
|
|
160
|
+
|
|
161
|
+
# Validate raw input
|
|
162
|
+
if not command.raw_input.strip():
|
|
163
|
+
errors.append("Raw input cannot be empty")
|
|
164
|
+
|
|
165
|
+
# Validate arguments don't contain control characters
|
|
166
|
+
for i, arg in enumerate(command.args):
|
|
167
|
+
if any(ord(c) < 32 for c in arg if c != '\t'):
|
|
168
|
+
errors.append(f"Argument {i+1} contains invalid control characters")
|
|
169
|
+
|
|
170
|
+
return errors
|
|
171
|
+
|
|
172
|
+
def get_command_signature(self, command: SlashCommand) -> str:
|
|
173
|
+
"""Get a string representation of the command for logging.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
command: Slash command to represent.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
String signature like "/save filename.txt --format=json"
|
|
180
|
+
"""
|
|
181
|
+
signature = f"/{command.name}"
|
|
182
|
+
|
|
183
|
+
if command.args:
|
|
184
|
+
signature += " " + " ".join(f'"{arg}"' if ' ' in arg else arg for arg in command.args)
|
|
185
|
+
|
|
186
|
+
return signature
|