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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. 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
+ }
@@ -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