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,267 @@
|
|
|
1
|
+
"""Plugin utility functions for introspection and safe method calling."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def has_method(obj: Any, method_name: str) -> bool:
|
|
11
|
+
"""Check if object has a callable method with given name.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
obj: Object to check.
|
|
15
|
+
method_name: Name of method to look for.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
True if object has callable method, False otherwise.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> class TestPlugin:
|
|
22
|
+
... def get_status(self): pass
|
|
23
|
+
>>> plugin = TestPlugin()
|
|
24
|
+
>>> has_method(plugin, "get_status")
|
|
25
|
+
True
|
|
26
|
+
>>> has_method(plugin, "missing_method")
|
|
27
|
+
False
|
|
28
|
+
"""
|
|
29
|
+
return hasattr(obj, method_name) and callable(getattr(obj, method_name, None))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def safe_call_method(obj: Any, method_name: str, *args, **kwargs) -> Optional[Any]:
|
|
33
|
+
"""Safely call a method on an object with error handling.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
obj: Object to call method on.
|
|
37
|
+
method_name: Name of method to call.
|
|
38
|
+
*args: Positional arguments to pass to method.
|
|
39
|
+
**kwargs: Keyword arguments to pass to method.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Method result or None if method doesn't exist or call failed.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> class TestPlugin:
|
|
46
|
+
... def get_config(self, key="default"):
|
|
47
|
+
... return {"key": key}
|
|
48
|
+
>>> plugin = TestPlugin()
|
|
49
|
+
>>> safe_call_method(plugin, "get_config", key="test")
|
|
50
|
+
{"key": "test"}
|
|
51
|
+
"""
|
|
52
|
+
if not has_method(obj, method_name):
|
|
53
|
+
logger.debug(f"Object {type(obj).__name__} has no method '{method_name}'")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
method = getattr(obj, method_name)
|
|
58
|
+
return method(*args, **kwargs)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
logger.error(f"Failed to call {type(obj).__name__}.{method_name}: {e}")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_plugin_metadata(plugin_class: Type) -> Dict[str, Any]:
|
|
65
|
+
"""Extract metadata from a plugin class.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
plugin_class: Plugin class to analyze.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with plugin metadata.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> class TestPlugin:
|
|
75
|
+
... '''Test plugin for demo.'''
|
|
76
|
+
... VERSION = "1.0.0"
|
|
77
|
+
... def get_default_config(self): return {}
|
|
78
|
+
>>> get_plugin_metadata(TestPlugin)
|
|
79
|
+
{
|
|
80
|
+
"name": "TestPlugin",
|
|
81
|
+
"docstring": "Test plugin for demo.",
|
|
82
|
+
"version": "1.0.0",
|
|
83
|
+
"has_config": True,
|
|
84
|
+
"methods": ["get_default_config"]
|
|
85
|
+
}
|
|
86
|
+
"""
|
|
87
|
+
metadata = {
|
|
88
|
+
"name": plugin_class.__name__,
|
|
89
|
+
"docstring": inspect.getdoc(plugin_class) or "",
|
|
90
|
+
"version": getattr(plugin_class, "VERSION", "unknown"),
|
|
91
|
+
"has_config": has_method(plugin_class, "get_default_config"),
|
|
92
|
+
"has_startup_info": has_method(plugin_class, "get_startup_info"),
|
|
93
|
+
"has_status": False,
|
|
94
|
+
"methods": []
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Check for common plugin methods
|
|
98
|
+
common_methods = [
|
|
99
|
+
"initialize", "register_hooks", "shutdown",
|
|
100
|
+
"get_default_config", "get_startup_info",
|
|
101
|
+
"get_status_line", "get_status_lines"
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for method_name in common_methods:
|
|
105
|
+
if has_method(plugin_class, method_name):
|
|
106
|
+
metadata["methods"].append(method_name)
|
|
107
|
+
if method_name.startswith("get_status"):
|
|
108
|
+
metadata["has_status"] = True
|
|
109
|
+
|
|
110
|
+
return metadata
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_plugin_interface(plugin_class: Type, required_methods: List[str] = None) -> Dict[str, Any]:
|
|
114
|
+
"""Validate that plugin class implements required interface.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
plugin_class: Plugin class to validate.
|
|
118
|
+
required_methods: List of required method names.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary with validation results.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
>>> class TestPlugin:
|
|
125
|
+
... def initialize(self): pass
|
|
126
|
+
>>> validate_plugin_interface(TestPlugin, ["initialize", "shutdown"])
|
|
127
|
+
{
|
|
128
|
+
"valid": False,
|
|
129
|
+
"missing_methods": ["shutdown"],
|
|
130
|
+
"has_methods": ["initialize"],
|
|
131
|
+
"errors": []
|
|
132
|
+
}
|
|
133
|
+
"""
|
|
134
|
+
if required_methods is None:
|
|
135
|
+
required_methods = ["initialize", "register_hooks", "shutdown"]
|
|
136
|
+
|
|
137
|
+
result = {
|
|
138
|
+
"valid": True,
|
|
139
|
+
"missing_methods": [],
|
|
140
|
+
"has_methods": [],
|
|
141
|
+
"errors": []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Check for required methods
|
|
145
|
+
for method_name in required_methods:
|
|
146
|
+
if has_method(plugin_class, method_name):
|
|
147
|
+
result["has_methods"].append(method_name)
|
|
148
|
+
else:
|
|
149
|
+
result["missing_methods"].append(method_name)
|
|
150
|
+
result["valid"] = False
|
|
151
|
+
|
|
152
|
+
# Check constructor signature
|
|
153
|
+
try:
|
|
154
|
+
init_signature = inspect.signature(plugin_class.__init__)
|
|
155
|
+
params = list(init_signature.parameters.keys())
|
|
156
|
+
|
|
157
|
+
# Skip 'self' parameter
|
|
158
|
+
if params and params[0] == "self":
|
|
159
|
+
params = params[1:]
|
|
160
|
+
|
|
161
|
+
# Common expected parameters for plugins
|
|
162
|
+
expected_params = ["name", "state_manager", "event_bus", "renderer", "config"]
|
|
163
|
+
missing_params = [p for p in expected_params if p not in params]
|
|
164
|
+
|
|
165
|
+
if missing_params:
|
|
166
|
+
result["errors"].append(f"Constructor missing parameters: {missing_params}")
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
result["errors"].append(f"Failed to inspect constructor: {e}")
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_plugin_config_safely(plugin_class: Type) -> Dict[str, Any]:
|
|
175
|
+
"""Safely get default configuration from plugin class.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
plugin_class: Plugin class to get config from.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Plugin's default configuration or empty dict if unavailable.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
if has_method(plugin_class, "get_default_config"):
|
|
185
|
+
config = plugin_class.get_default_config()
|
|
186
|
+
if isinstance(config, dict):
|
|
187
|
+
return config
|
|
188
|
+
else:
|
|
189
|
+
logger.warning(f"Plugin {plugin_class.__name__} returned non-dict config: {type(config)}")
|
|
190
|
+
else:
|
|
191
|
+
logger.debug(f"Plugin {plugin_class.__name__} has no get_default_config method")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.error(f"Failed to get config from plugin {plugin_class.__name__}: {e}")
|
|
194
|
+
|
|
195
|
+
return {}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def instantiate_plugin_safely(plugin_class: Type, **kwargs) -> Optional[Any]:
|
|
199
|
+
"""Safely instantiate a plugin with error handling.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
plugin_class: Plugin class to instantiate.
|
|
203
|
+
**kwargs: Arguments to pass to plugin constructor.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Plugin instance or None if instantiation failed.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
# Validate that this looks like a plugin class
|
|
210
|
+
if not plugin_class.__name__.endswith('Plugin'):
|
|
211
|
+
logger.warning(f"Class {plugin_class.__name__} doesn't follow plugin naming convention")
|
|
212
|
+
|
|
213
|
+
# Try to instantiate
|
|
214
|
+
instance = plugin_class(**kwargs)
|
|
215
|
+
logger.info(f"Successfully instantiated plugin: {plugin_class.__name__}")
|
|
216
|
+
return instance
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
logger.error(f"Failed to instantiate plugin {plugin_class.__name__}: {e}")
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def collect_plugin_status_safely(plugin_instance: Any, plugin_name: str) -> Dict[str, List[str]]:
|
|
224
|
+
"""Safely collect status information from plugin instance.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
plugin_instance: Plugin instance to get status from.
|
|
228
|
+
plugin_name: Name of plugin (for logging).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dictionary with status areas A, B, C containing lists of status lines.
|
|
232
|
+
"""
|
|
233
|
+
status_areas = {"A": [], "B": [], "C": []}
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
# Try new format first: get_status_lines() returning dict
|
|
237
|
+
if has_method(plugin_instance, 'get_status_lines'):
|
|
238
|
+
plugin_status = safe_call_method(plugin_instance, 'get_status_lines')
|
|
239
|
+
|
|
240
|
+
if isinstance(plugin_status, dict):
|
|
241
|
+
# New format: plugin returns dict with areas
|
|
242
|
+
for area, lines in plugin_status.items():
|
|
243
|
+
if area in status_areas:
|
|
244
|
+
if isinstance(lines, list):
|
|
245
|
+
status_areas[area].extend([line for line in lines if line and line.strip()])
|
|
246
|
+
elif isinstance(lines, str) and lines.strip():
|
|
247
|
+
status_areas[area].append(lines.strip())
|
|
248
|
+
return status_areas
|
|
249
|
+
elif isinstance(plugin_status, list):
|
|
250
|
+
# Legacy format: plugin returns list, put in area A
|
|
251
|
+
status_areas["A"].extend([line for line in plugin_status if line and line.strip()])
|
|
252
|
+
return status_areas
|
|
253
|
+
elif isinstance(plugin_status, str) and plugin_status.strip():
|
|
254
|
+
# Legacy format: plugin returns string, put in area A
|
|
255
|
+
status_areas["A"].append(plugin_status.strip())
|
|
256
|
+
return status_areas
|
|
257
|
+
|
|
258
|
+
# Try legacy format: get_status_line() returning string
|
|
259
|
+
if has_method(plugin_instance, 'get_status_line'):
|
|
260
|
+
plugin_status = safe_call_method(plugin_instance, 'get_status_line')
|
|
261
|
+
if isinstance(plugin_status, str) and plugin_status.strip():
|
|
262
|
+
status_areas["A"].append(plugin_status.strip())
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.warning(f"Failed to get status from plugin {plugin_name}: {e}")
|
|
266
|
+
|
|
267
|
+
return status_areas
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""System prompt dynamic command renderer.
|
|
2
|
+
|
|
3
|
+
Processes <trender>command</trender> tags in system prompts by executing
|
|
4
|
+
the commands and replacing tags with their output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
from typing import Dict, List, Tuple
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PromptRenderer:
|
|
16
|
+
"""Renders dynamic content in system prompts by executing commands."""
|
|
17
|
+
|
|
18
|
+
# Pattern to match <trender>command</trender> tags
|
|
19
|
+
# Excludes matches that are inside backticks (code examples)
|
|
20
|
+
TRENDER_PATTERN = re.compile(r'(?<!`)<trender>(.*?)</trender>(?!`)', re.DOTALL)
|
|
21
|
+
|
|
22
|
+
def __init__(self, timeout: int = 5):
|
|
23
|
+
"""Initialize the prompt renderer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
timeout: Maximum seconds to wait for each command execution.
|
|
27
|
+
"""
|
|
28
|
+
self.timeout = timeout
|
|
29
|
+
self._command_cache: Dict[str, str] = {}
|
|
30
|
+
|
|
31
|
+
def render(self, prompt_content: str) -> str:
|
|
32
|
+
"""Render all <trender> tags in the prompt content.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
prompt_content: System prompt content with <trender> tags.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Processed prompt with commands replaced by their output.
|
|
39
|
+
"""
|
|
40
|
+
if not prompt_content:
|
|
41
|
+
return prompt_content
|
|
42
|
+
|
|
43
|
+
# Find all trender tags
|
|
44
|
+
matches = list(self.TRENDER_PATTERN.finditer(prompt_content))
|
|
45
|
+
|
|
46
|
+
if not matches:
|
|
47
|
+
logger.debug("No <trender> tags found in system prompt")
|
|
48
|
+
return prompt_content
|
|
49
|
+
|
|
50
|
+
logger.info(f"Found {len(matches)} <trender> tag(s) to process")
|
|
51
|
+
|
|
52
|
+
# Process matches in reverse order to maintain string positions
|
|
53
|
+
result = prompt_content
|
|
54
|
+
for match in reversed(matches):
|
|
55
|
+
command = match.group(1).strip()
|
|
56
|
+
start_pos = match.start()
|
|
57
|
+
end_pos = match.end()
|
|
58
|
+
|
|
59
|
+
# Execute command and get output
|
|
60
|
+
output = self._execute_command(command)
|
|
61
|
+
|
|
62
|
+
# Replace the tag with the output
|
|
63
|
+
result = result[:start_pos] + output + result[end_pos:]
|
|
64
|
+
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
def _execute_command(self, command: str) -> str:
|
|
68
|
+
"""Execute a shell command and return its output.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
command: Shell command to execute.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Command output or error message.
|
|
75
|
+
"""
|
|
76
|
+
# Check cache first
|
|
77
|
+
if command in self._command_cache:
|
|
78
|
+
logger.debug(f"Using cached output for: {command}")
|
|
79
|
+
return self._command_cache[command]
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
logger.debug(f"Executing trender command: {command}")
|
|
83
|
+
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
command,
|
|
86
|
+
shell=True,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
timeout=self.timeout,
|
|
90
|
+
cwd="." # Execute in current directory
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Get output (prefer stdout, fallback to stderr)
|
|
94
|
+
output = result.stdout if result.stdout else result.stderr
|
|
95
|
+
output = output.strip()
|
|
96
|
+
|
|
97
|
+
if result.returncode != 0:
|
|
98
|
+
error_msg = f"[trender error: command exited with code {result.returncode}]"
|
|
99
|
+
if result.stderr:
|
|
100
|
+
error_msg += f"\n{result.stderr.strip()}"
|
|
101
|
+
logger.warning(f"Command failed: {command} (exit code: {result.returncode})")
|
|
102
|
+
output = error_msg
|
|
103
|
+
|
|
104
|
+
# Cache successful results
|
|
105
|
+
if result.returncode == 0:
|
|
106
|
+
self._command_cache[command] = output
|
|
107
|
+
|
|
108
|
+
logger.debug(f"Command output ({len(output)} chars): {output[:100]}")
|
|
109
|
+
return output
|
|
110
|
+
|
|
111
|
+
except subprocess.TimeoutExpired:
|
|
112
|
+
error_msg = f"[trender error: command timed out after {self.timeout}s]"
|
|
113
|
+
logger.error(f"Command timed out: {command}")
|
|
114
|
+
return error_msg
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
error_msg = f"[trender error: {type(e).__name__}: {str(e)}]"
|
|
118
|
+
logger.error(f"Failed to execute command '{command}': {e}")
|
|
119
|
+
return error_msg
|
|
120
|
+
|
|
121
|
+
def clear_cache(self):
|
|
122
|
+
"""Clear the command output cache."""
|
|
123
|
+
self._command_cache.clear()
|
|
124
|
+
logger.debug("Cleared trender command cache")
|
|
125
|
+
|
|
126
|
+
def get_all_commands(self, prompt_content: str) -> List[str]:
|
|
127
|
+
"""Extract all commands from trender tags without executing.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
prompt_content: System prompt content with <trender> tags.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of commands found in trender tags.
|
|
134
|
+
"""
|
|
135
|
+
matches = self.TRENDER_PATTERN.findall(prompt_content)
|
|
136
|
+
commands = [cmd.strip() for cmd in matches]
|
|
137
|
+
return commands
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def render_system_prompt(prompt_content: str, timeout: int = 5) -> str:
|
|
141
|
+
"""Convenience function to render a system prompt.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
prompt_content: System prompt content with <trender> tags.
|
|
145
|
+
timeout: Maximum seconds to wait for each command execution.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Processed prompt with commands replaced by their output.
|
|
149
|
+
"""
|
|
150
|
+
renderer = PromptRenderer(timeout=timeout)
|
|
151
|
+
return renderer.render(prompt_content)
|