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,502 @@
|
|
|
1
|
+
"""Save conversation plugin for exporting chat transcripts.
|
|
2
|
+
|
|
3
|
+
Provides /save command to export conversations to file or clipboard
|
|
4
|
+
in various formats (transcript, markdown, jsonl).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from core.events.models import CommandDefinition, CommandCategory, CommandMode
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SaveConversationPlugin:
|
|
19
|
+
"""Plugin for saving conversations to file or clipboard."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, name: str = "save_conversation", state_manager=None, event_bus=None,
|
|
22
|
+
renderer=None, config=None) -> None:
|
|
23
|
+
"""Initialize the save conversation plugin.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name: Plugin name (default: "save_conversation")
|
|
27
|
+
state_manager: State manager instance
|
|
28
|
+
event_bus: Event bus instance
|
|
29
|
+
renderer: Terminal renderer instance
|
|
30
|
+
config: Configuration manager instance
|
|
31
|
+
"""
|
|
32
|
+
self.name = name
|
|
33
|
+
self.version = "1.0.0"
|
|
34
|
+
self.description = "Save conversations to file or clipboard"
|
|
35
|
+
self.enabled = True
|
|
36
|
+
self.logger = logger
|
|
37
|
+
|
|
38
|
+
# Store injected dependencies
|
|
39
|
+
self.state_manager = state_manager
|
|
40
|
+
self.event_bus = event_bus
|
|
41
|
+
self.renderer = renderer
|
|
42
|
+
self.config_manager = config
|
|
43
|
+
|
|
44
|
+
# References to be set during initialize()
|
|
45
|
+
self.command_registry = None
|
|
46
|
+
self.llm_service = None
|
|
47
|
+
self.config = None
|
|
48
|
+
|
|
49
|
+
async def initialize(self, event_bus, config, **kwargs) -> None:
|
|
50
|
+
"""Initialize the plugin and register commands.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
event_bus: Application event bus.
|
|
54
|
+
config: Configuration manager.
|
|
55
|
+
**kwargs: Additional initialization parameters.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
self.config = config
|
|
59
|
+
self.command_registry = kwargs.get('command_registry')
|
|
60
|
+
self.llm_service = kwargs.get('llm_service')
|
|
61
|
+
|
|
62
|
+
if not self.command_registry:
|
|
63
|
+
self.logger.warning("No command registry provided, /save not registered")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if not self.llm_service:
|
|
67
|
+
self.logger.warning("No LLM service provided, /save may not work")
|
|
68
|
+
|
|
69
|
+
# Register the /save command
|
|
70
|
+
self._register_commands()
|
|
71
|
+
|
|
72
|
+
self.logger.info("Save conversation plugin initialized successfully")
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.error(f"Error initializing save conversation plugin: {e}")
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
def _register_commands(self) -> None:
|
|
79
|
+
"""Register all plugin commands."""
|
|
80
|
+
save_command = CommandDefinition(
|
|
81
|
+
name="save",
|
|
82
|
+
description="Save conversation to file or clipboard",
|
|
83
|
+
handler=self._handle_save_command,
|
|
84
|
+
plugin_name=self.name,
|
|
85
|
+
aliases=["export", "transcript"],
|
|
86
|
+
mode=CommandMode.INSTANT,
|
|
87
|
+
category=CommandCategory.CONVERSATION,
|
|
88
|
+
icon="[SAVE]"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
self.command_registry.register_command(save_command)
|
|
92
|
+
self.logger.info("Registered /save command")
|
|
93
|
+
|
|
94
|
+
async def _handle_save_command(self, command) -> str:
|
|
95
|
+
"""Handle the /save command.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
command: SlashCommand object with parsed command data.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Status message about the save operation.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
# Parse arguments: /save [format] [destination]
|
|
105
|
+
# Formats: transcript (default), markdown, jsonl
|
|
106
|
+
# Destinations: file (default), clipboard, both
|
|
107
|
+
|
|
108
|
+
args = command.args if hasattr(command, 'args') else []
|
|
109
|
+
|
|
110
|
+
# Get configuration
|
|
111
|
+
save_format = self.config.get("plugins.save_conversation.default_format", "transcript")
|
|
112
|
+
save_to = self.config.get("plugins.save_conversation.default_destination", "file")
|
|
113
|
+
auto_timestamp = self.config.get("plugins.save_conversation.auto_timestamp", True)
|
|
114
|
+
output_dir = self.config.get("plugins.save_conversation.output_directory", "logs/transcripts")
|
|
115
|
+
|
|
116
|
+
# Parse command arguments
|
|
117
|
+
if len(args) >= 1:
|
|
118
|
+
save_format = args[0].lower()
|
|
119
|
+
if len(args) >= 2:
|
|
120
|
+
save_to = args[1].lower()
|
|
121
|
+
|
|
122
|
+
# Validate format
|
|
123
|
+
if save_format not in ["transcript", "markdown", "jsonl", "raw"]:
|
|
124
|
+
return f"Error: Invalid format '{save_format}'. Use: transcript, markdown, jsonl, or raw"
|
|
125
|
+
|
|
126
|
+
# Validate destination
|
|
127
|
+
if save_to not in ["file", "clipboard", "both"]:
|
|
128
|
+
return f"Error: Invalid destination '{save_to}'. Use: file, clipboard, or both"
|
|
129
|
+
|
|
130
|
+
# Get conversation content
|
|
131
|
+
if not self.llm_service:
|
|
132
|
+
return "Error: LLM service not available"
|
|
133
|
+
|
|
134
|
+
# Get messages from llm_service conversation_history
|
|
135
|
+
conversation_history = self.llm_service.conversation_history
|
|
136
|
+
if not conversation_history:
|
|
137
|
+
return "No conversation to save"
|
|
138
|
+
|
|
139
|
+
# Convert ConversationMessage objects to dict format
|
|
140
|
+
# Preserves EXACT content as sent to/received from API
|
|
141
|
+
messages = []
|
|
142
|
+
for msg in conversation_history:
|
|
143
|
+
msg_dict = {
|
|
144
|
+
"role": msg.role,
|
|
145
|
+
"content": msg.content, # Exact content - no processing
|
|
146
|
+
}
|
|
147
|
+
# Use actual timestamp from message if available
|
|
148
|
+
if hasattr(msg, 'timestamp') and msg.timestamp:
|
|
149
|
+
msg_dict["timestamp"] = msg.timestamp.isoformat() if hasattr(msg.timestamp, 'isoformat') else str(msg.timestamp)
|
|
150
|
+
else:
|
|
151
|
+
msg_dict["timestamp"] = datetime.now().isoformat()
|
|
152
|
+
|
|
153
|
+
# Include thinking if present (for debugging)
|
|
154
|
+
if hasattr(msg, 'thinking') and msg.thinking:
|
|
155
|
+
msg_dict["thinking"] = msg.thinking
|
|
156
|
+
|
|
157
|
+
messages.append(msg_dict)
|
|
158
|
+
|
|
159
|
+
# Format the conversation
|
|
160
|
+
formatted_content = self._format_conversation(messages, save_format)
|
|
161
|
+
|
|
162
|
+
# Save to file
|
|
163
|
+
saved_path = None
|
|
164
|
+
if save_to in ["file", "both"]:
|
|
165
|
+
saved_path = self._save_to_file(formatted_content, output_dir, save_format, auto_timestamp)
|
|
166
|
+
|
|
167
|
+
# Copy to clipboard
|
|
168
|
+
if save_to in ["clipboard", "both"]:
|
|
169
|
+
self._copy_to_clipboard(formatted_content)
|
|
170
|
+
|
|
171
|
+
# Return status message
|
|
172
|
+
if save_to == "both":
|
|
173
|
+
return f"Conversation saved to {saved_path} and copied to clipboard"
|
|
174
|
+
elif save_to == "clipboard":
|
|
175
|
+
return f"Conversation copied to clipboard ({len(messages)} messages)"
|
|
176
|
+
else:
|
|
177
|
+
return f"Conversation saved to {saved_path}"
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self.logger.error(f"Error handling /save command: {e}")
|
|
181
|
+
return f"Error saving conversation: {str(e)}"
|
|
182
|
+
|
|
183
|
+
def _format_conversation(self, messages, format_type: str) -> str:
|
|
184
|
+
"""Format conversation messages based on requested format.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
messages: List of conversation messages.
|
|
188
|
+
format_type: Format type (transcript, markdown, jsonl, raw).
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Formatted conversation string.
|
|
192
|
+
"""
|
|
193
|
+
if format_type == "raw":
|
|
194
|
+
return self._format_as_raw_api(messages)
|
|
195
|
+
elif format_type == "transcript":
|
|
196
|
+
return self._format_as_transcript(messages)
|
|
197
|
+
elif format_type == "markdown":
|
|
198
|
+
return self._format_as_markdown(messages)
|
|
199
|
+
elif format_type == "jsonl":
|
|
200
|
+
return self._format_as_jsonl(messages)
|
|
201
|
+
else:
|
|
202
|
+
return self._format_as_transcript(messages)
|
|
203
|
+
|
|
204
|
+
def _format_as_transcript(self, messages) -> str:
|
|
205
|
+
"""Format messages as raw transcript.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
messages: List of conversation messages.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Transcript formatted string.
|
|
212
|
+
"""
|
|
213
|
+
lines = []
|
|
214
|
+
|
|
215
|
+
for msg in messages:
|
|
216
|
+
role = msg.get("role", "unknown")
|
|
217
|
+
content = msg.get("content", "")
|
|
218
|
+
|
|
219
|
+
# Map role to section header
|
|
220
|
+
if role == "system":
|
|
221
|
+
lines.append("--- system_prompt ---")
|
|
222
|
+
elif role == "user":
|
|
223
|
+
lines.append("\n--- user ---")
|
|
224
|
+
elif role == "assistant":
|
|
225
|
+
lines.append("\n--- llm ---")
|
|
226
|
+
else:
|
|
227
|
+
lines.append(f"\n--- {role} ---")
|
|
228
|
+
|
|
229
|
+
lines.append(content)
|
|
230
|
+
|
|
231
|
+
return "\n".join(lines)
|
|
232
|
+
|
|
233
|
+
def _format_as_markdown(self, messages) -> str:
|
|
234
|
+
"""Format messages as markdown.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
messages: List of conversation messages.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Markdown formatted string.
|
|
241
|
+
"""
|
|
242
|
+
lines = ["# Conversation Transcript", ""]
|
|
243
|
+
|
|
244
|
+
# Add metadata
|
|
245
|
+
if messages:
|
|
246
|
+
first_timestamp = messages[0].get("timestamp", "")
|
|
247
|
+
last_timestamp = messages[-1].get("timestamp", "")
|
|
248
|
+
lines.append(f"**Started:** {first_timestamp}")
|
|
249
|
+
lines.append(f"**Ended:** {last_timestamp}")
|
|
250
|
+
lines.append(f"**Messages:** {len(messages)}")
|
|
251
|
+
lines.append("")
|
|
252
|
+
lines.append("---")
|
|
253
|
+
lines.append("")
|
|
254
|
+
|
|
255
|
+
for i, msg in enumerate(messages):
|
|
256
|
+
role = msg.get("role", "unknown")
|
|
257
|
+
content = msg.get("content", "")
|
|
258
|
+
timestamp = msg.get("timestamp", "")
|
|
259
|
+
|
|
260
|
+
# Format based on role
|
|
261
|
+
if role == "system":
|
|
262
|
+
lines.append("## System Prompt")
|
|
263
|
+
lines.append("")
|
|
264
|
+
lines.append(f"```\n{content}\n```")
|
|
265
|
+
elif role == "user":
|
|
266
|
+
lines.append(f"## User Message {i+1}")
|
|
267
|
+
if timestamp:
|
|
268
|
+
lines.append(f"*{timestamp}*")
|
|
269
|
+
lines.append("")
|
|
270
|
+
lines.append(content)
|
|
271
|
+
elif role == "assistant":
|
|
272
|
+
lines.append(f"## Assistant Response {i+1}")
|
|
273
|
+
if timestamp:
|
|
274
|
+
lines.append(f"*{timestamp}*")
|
|
275
|
+
lines.append("")
|
|
276
|
+
lines.append(content)
|
|
277
|
+
else:
|
|
278
|
+
lines.append(f"## {role.title()} {i+1}")
|
|
279
|
+
lines.append("")
|
|
280
|
+
lines.append(content)
|
|
281
|
+
|
|
282
|
+
lines.append("")
|
|
283
|
+
lines.append("---")
|
|
284
|
+
lines.append("")
|
|
285
|
+
|
|
286
|
+
return "\n".join(lines)
|
|
287
|
+
|
|
288
|
+
def _format_as_jsonl(self, messages) -> str:
|
|
289
|
+
"""Format messages as JSONL (JSON Lines).
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
messages: List of conversation messages.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
JSONL formatted string.
|
|
296
|
+
"""
|
|
297
|
+
import json
|
|
298
|
+
lines = []
|
|
299
|
+
|
|
300
|
+
for msg in messages:
|
|
301
|
+
lines.append(json.dumps(msg))
|
|
302
|
+
|
|
303
|
+
return "\n".join(lines)
|
|
304
|
+
|
|
305
|
+
def _format_as_raw_api(self, messages) -> str:
|
|
306
|
+
"""Format messages as exact API payload JSON.
|
|
307
|
+
|
|
308
|
+
This format mirrors EXACTLY what is sent to and received from the LLM API.
|
|
309
|
+
Useful for debugging, replay, and verification.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
messages: List of conversation messages.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
JSON formatted string matching API payload structure.
|
|
316
|
+
"""
|
|
317
|
+
import json
|
|
318
|
+
|
|
319
|
+
# Build the exact payload structure sent to the API
|
|
320
|
+
api_messages = []
|
|
321
|
+
for msg in messages:
|
|
322
|
+
api_messages.append({
|
|
323
|
+
"role": msg.get("role"),
|
|
324
|
+
"content": msg.get("content")
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
# Get model info from config if available
|
|
328
|
+
model = "unknown"
|
|
329
|
+
temperature = 0.7
|
|
330
|
+
if self.config:
|
|
331
|
+
model = self.config.get("core.llm.model", "unknown")
|
|
332
|
+
temperature = self.config.get("core.llm.temperature", 0.7)
|
|
333
|
+
|
|
334
|
+
payload = {
|
|
335
|
+
"model": model,
|
|
336
|
+
"messages": api_messages,
|
|
337
|
+
"temperature": temperature,
|
|
338
|
+
"metadata": {
|
|
339
|
+
"exported_at": datetime.now().isoformat(),
|
|
340
|
+
"message_count": len(messages),
|
|
341
|
+
"format": "raw_api_payload"
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return json.dumps(payload, indent=2, ensure_ascii=False)
|
|
346
|
+
|
|
347
|
+
def _save_to_file(self, content: str, output_dir: str, format_type: str, auto_timestamp: bool) -> Path:
|
|
348
|
+
"""Save content to file.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
content: Content to save.
|
|
352
|
+
output_dir: Output directory path.
|
|
353
|
+
format_type: Format type for file extension.
|
|
354
|
+
auto_timestamp: Whether to add timestamp to filename.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Path to saved file.
|
|
358
|
+
"""
|
|
359
|
+
# Create output directory
|
|
360
|
+
from core.utils.config_utils import get_config_directory
|
|
361
|
+
config_dir = get_config_directory()
|
|
362
|
+
|
|
363
|
+
# Handle relative paths
|
|
364
|
+
if not output_dir.startswith('/'):
|
|
365
|
+
save_dir = config_dir / output_dir
|
|
366
|
+
else:
|
|
367
|
+
save_dir = Path(output_dir)
|
|
368
|
+
|
|
369
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
370
|
+
|
|
371
|
+
# Generate filename
|
|
372
|
+
if auto_timestamp:
|
|
373
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
374
|
+
filename = f"conversation_{timestamp}"
|
|
375
|
+
else:
|
|
376
|
+
filename = "conversation"
|
|
377
|
+
|
|
378
|
+
# Add extension based on format
|
|
379
|
+
if format_type == "raw":
|
|
380
|
+
filename += ".json"
|
|
381
|
+
elif format_type == "jsonl":
|
|
382
|
+
filename += ".jsonl"
|
|
383
|
+
elif format_type == "markdown":
|
|
384
|
+
filename += ".md"
|
|
385
|
+
else:
|
|
386
|
+
filename += ".txt"
|
|
387
|
+
|
|
388
|
+
filepath = save_dir / filename
|
|
389
|
+
|
|
390
|
+
# Write to file
|
|
391
|
+
filepath.write_text(content, encoding='utf-8')
|
|
392
|
+
|
|
393
|
+
self.logger.info(f"Saved conversation to: {filepath}")
|
|
394
|
+
return filepath
|
|
395
|
+
|
|
396
|
+
def _copy_to_clipboard(self, content: str) -> bool:
|
|
397
|
+
"""Copy content to system clipboard.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
content: Content to copy.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
True if successful, False otherwise.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
# Try pbcopy (macOS)
|
|
407
|
+
try:
|
|
408
|
+
process = subprocess.Popen(
|
|
409
|
+
['pbcopy'],
|
|
410
|
+
stdin=subprocess.PIPE,
|
|
411
|
+
stdout=subprocess.PIPE,
|
|
412
|
+
stderr=subprocess.PIPE
|
|
413
|
+
)
|
|
414
|
+
process.communicate(input=content.encode('utf-8'))
|
|
415
|
+
self.logger.info("Copied to clipboard using pbcopy")
|
|
416
|
+
return True
|
|
417
|
+
except FileNotFoundError:
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
# Try xclip (Linux)
|
|
421
|
+
try:
|
|
422
|
+
process = subprocess.Popen(
|
|
423
|
+
['xclip', '-selection', 'clipboard'],
|
|
424
|
+
stdin=subprocess.PIPE,
|
|
425
|
+
stdout=subprocess.PIPE,
|
|
426
|
+
stderr=subprocess.PIPE
|
|
427
|
+
)
|
|
428
|
+
process.communicate(input=content.encode('utf-8'))
|
|
429
|
+
self.logger.info("Copied to clipboard using xclip")
|
|
430
|
+
return True
|
|
431
|
+
except FileNotFoundError:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
# Try xsel (Linux alternative)
|
|
435
|
+
try:
|
|
436
|
+
process = subprocess.Popen(
|
|
437
|
+
['xsel', '--clipboard', '--input'],
|
|
438
|
+
stdin=subprocess.PIPE,
|
|
439
|
+
stdout=subprocess.PIPE,
|
|
440
|
+
stderr=subprocess.PIPE
|
|
441
|
+
)
|
|
442
|
+
process.communicate(input=content.encode('utf-8'))
|
|
443
|
+
self.logger.info("Copied to clipboard using xsel")
|
|
444
|
+
return True
|
|
445
|
+
except FileNotFoundError:
|
|
446
|
+
pass
|
|
447
|
+
|
|
448
|
+
# Try wl-copy (Wayland)
|
|
449
|
+
try:
|
|
450
|
+
process = subprocess.Popen(
|
|
451
|
+
['wl-copy'],
|
|
452
|
+
stdin=subprocess.PIPE,
|
|
453
|
+
stdout=subprocess.PIPE,
|
|
454
|
+
stderr=subprocess.PIPE
|
|
455
|
+
)
|
|
456
|
+
process.communicate(input=content.encode('utf-8'))
|
|
457
|
+
self.logger.info("Copied to clipboard using wl-copy")
|
|
458
|
+
return True
|
|
459
|
+
except FileNotFoundError:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
self.logger.warning("No clipboard utility found (pbcopy, xclip, xsel, wl-copy)")
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
self.logger.error(f"Error copying to clipboard: {e}")
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
async def shutdown(self) -> None:
|
|
470
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
471
|
+
try:
|
|
472
|
+
self.logger.info("Save conversation plugin shutdown completed")
|
|
473
|
+
except Exception as e:
|
|
474
|
+
self.logger.error(f"Error shutting down save conversation plugin: {e}")
|
|
475
|
+
|
|
476
|
+
async def register_hooks(self) -> None:
|
|
477
|
+
"""Register event hooks for the plugin."""
|
|
478
|
+
# This plugin doesn't need hooks, just commands
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def get_status_line(self) -> str:
|
|
482
|
+
"""Get status line information for the plugin.
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Status line string.
|
|
486
|
+
"""
|
|
487
|
+
return "Save: /save"
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def get_default_config() -> Dict[str, Any]:
|
|
491
|
+
"""Get default configuration for save conversation plugin."""
|
|
492
|
+
return {
|
|
493
|
+
"plugins": {
|
|
494
|
+
"save_conversation": {
|
|
495
|
+
"enabled": True,
|
|
496
|
+
"default_format": "transcript",
|
|
497
|
+
"default_destination": "file",
|
|
498
|
+
"auto_timestamp": True,
|
|
499
|
+
"output_directory": "logs/transcripts"
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""System commands plugin for core application functionality."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
from core.commands.system_commands import SystemCommandsPlugin as CoreSystemCommandsPlugin
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SystemCommandsPlugin:
|
|
11
|
+
"""Plugin wrapper for system commands integration.
|
|
12
|
+
|
|
13
|
+
Provides the system commands as a plugin that gets loaded
|
|
14
|
+
during application initialization.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
"""Initialize the system commands plugin wrapper."""
|
|
19
|
+
self.name = "system_commands"
|
|
20
|
+
self.version = "1.0.0"
|
|
21
|
+
self.description = "Core system commands (/help, /config, /status, etc.)"
|
|
22
|
+
self.enabled = True
|
|
23
|
+
self.system_commands = None
|
|
24
|
+
self.logger = logger
|
|
25
|
+
|
|
26
|
+
async def initialize(self, event_bus, config, **kwargs) -> None:
|
|
27
|
+
"""Initialize the plugin and register system commands.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
event_bus: Application event bus.
|
|
31
|
+
config: Configuration manager.
|
|
32
|
+
**kwargs: Additional initialization parameters.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
# Get command registry from input handler if available
|
|
36
|
+
command_registry = kwargs.get('command_registry')
|
|
37
|
+
if not command_registry:
|
|
38
|
+
self.logger.warning("No command registry provided, system commands not registered")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Create and initialize system commands
|
|
42
|
+
self.system_commands = CoreSystemCommandsPlugin(
|
|
43
|
+
command_registry=command_registry,
|
|
44
|
+
event_bus=event_bus,
|
|
45
|
+
config_manager=config
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Register all system commands
|
|
49
|
+
self.system_commands.register_commands()
|
|
50
|
+
|
|
51
|
+
self.logger.info("System commands plugin initialized successfully")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
self.logger.error(f"Error initializing system commands plugin: {e}")
|
|
55
|
+
raise
|
|
56
|
+
|
|
57
|
+
async def shutdown(self) -> None:
|
|
58
|
+
"""Shutdown the plugin and cleanup resources."""
|
|
59
|
+
try:
|
|
60
|
+
if self.system_commands:
|
|
61
|
+
# Unregister commands would happen here if needed
|
|
62
|
+
self.logger.info("System commands plugin shutdown completed")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
self.logger.error(f"Error shutting down system commands plugin: {e}")
|
|
66
|
+
|
|
67
|
+
def get_status_line(self) -> str:
|
|
68
|
+
"""Get status line information for the plugin.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Status line string.
|
|
72
|
+
"""
|
|
73
|
+
if self.system_commands:
|
|
74
|
+
return "System commands active"
|
|
75
|
+
return "System commands inactive"
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def get_default_config() -> Dict[str, Any]:
|
|
79
|
+
"""Get default configuration for system commands plugin."""
|
|
80
|
+
return {
|
|
81
|
+
"plugins": {
|
|
82
|
+
"system_commands": {
|
|
83
|
+
"enabled": True
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async def register_hooks(self) -> None:
|
|
89
|
+
"""Register event hooks for the plugin.
|
|
90
|
+
|
|
91
|
+
System commands don't need additional hooks beyond command registration.
|
|
92
|
+
"""
|
|
93
|
+
pass
|