omnimancer-cli 0.1.0__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 (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,352 @@
1
+ """
2
+ Conversation persistence manager for Omnimancer.
3
+
4
+ This module handles saving, loading, and managing conversation histories
5
+ including serialization, file management, and conversation organization.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime
10
+ from typing import Dict, List, Optional
11
+
12
+ from ..utils.errors import ConversationError
13
+ from .models import ChatContext, ChatMessage, MessageRole
14
+
15
+
16
+ class ConversationManager:
17
+ """
18
+ Manages conversation persistence and file operations.
19
+
20
+ This class handles saving conversations to files, loading them back,
21
+ and managing conversation file organization.
22
+ """
23
+
24
+ def __init__(self, storage_path):
25
+ """
26
+ Initialize the conversation manager.
27
+
28
+ Args:
29
+ storage_path: Base path for storing conversation files (str or Path)
30
+ """
31
+ from pathlib import Path
32
+
33
+ # Always ensure Path object and expand user home directory
34
+ if isinstance(storage_path, str):
35
+ self.storage_path = Path(storage_path).expanduser()
36
+ else:
37
+ self.storage_path = Path(storage_path).expanduser()
38
+ self.conversations_dir = self.storage_path / "conversations"
39
+ self._ensure_directories()
40
+
41
+ def _ensure_directories(self) -> None:
42
+ """Ensure required directories exist."""
43
+ self.conversations_dir.mkdir(parents=True, exist_ok=True)
44
+
45
+ def save_conversation(
46
+ self,
47
+ context: ChatContext,
48
+ filename: Optional[str] = None,
49
+ metadata: Optional[Dict] = None,
50
+ ) -> str:
51
+ """
52
+ Save a conversation to a file.
53
+
54
+ Args:
55
+ context: Chat context to save
56
+ filename: Optional filename (auto-generated if not provided)
57
+ metadata: Optional metadata to include
58
+
59
+ Returns:
60
+ Filename of the saved conversation
61
+
62
+ Raises:
63
+ ConversationError: If saving fails
64
+ """
65
+ try:
66
+ # Generate filename if not provided
67
+ if not filename:
68
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
69
+ filename = f"conversation_{timestamp}.json"
70
+
71
+ # Ensure .json extension
72
+ if not filename.endswith(".json"):
73
+ filename += ".json"
74
+
75
+ # Prepare conversation data
76
+ conversation_data = {
77
+ "version": "1.0",
78
+ "created_at": datetime.now().isoformat(),
79
+ "session_id": context.session_id,
80
+ "current_model": context.current_model,
81
+ "max_context_length": context.max_context_length,
82
+ "metadata": metadata or {},
83
+ "messages": [
84
+ {
85
+ "role": msg.role.value,
86
+ "content": msg.content,
87
+ "timestamp": msg.timestamp.isoformat(),
88
+ "model_used": msg.model_used,
89
+ }
90
+ for msg in context.messages
91
+ ],
92
+ }
93
+
94
+ # Save to file
95
+ file_path = self.conversations_dir / filename
96
+ with open(file_path, "w", encoding="utf-8") as f:
97
+ json.dump(conversation_data, f, indent=2, ensure_ascii=False)
98
+
99
+ return filename
100
+
101
+ except Exception as e:
102
+ raise ConversationError(f"Failed to save conversation: {e}")
103
+
104
+ def load_conversation(self, filename: str) -> ChatContext:
105
+ """
106
+ Load a conversation from a file.
107
+
108
+ Args:
109
+ filename: Name of the conversation file
110
+
111
+ Returns:
112
+ Loaded ChatContext
113
+
114
+ Raises:
115
+ ConversationError: If loading fails
116
+ """
117
+ try:
118
+ # Ensure .json extension
119
+ if not filename.endswith(".json"):
120
+ filename += ".json"
121
+
122
+ file_path = self.conversations_dir / filename
123
+
124
+ if not file_path.exists():
125
+ raise ConversationError(f"Conversation file not found: {filename}")
126
+
127
+ # Load conversation data
128
+ with open(file_path, "r", encoding="utf-8") as f:
129
+ conversation_data = json.load(f)
130
+
131
+ # Validate version
132
+ version = conversation_data.get("version", "1.0")
133
+ if version != "1.0":
134
+ raise ConversationError(
135
+ f"Unsupported conversation file version: {version}"
136
+ )
137
+
138
+ # Reconstruct messages
139
+ messages = []
140
+ for msg_data in conversation_data.get("messages", []):
141
+ message = ChatMessage(
142
+ role=MessageRole(msg_data["role"]),
143
+ content=msg_data["content"],
144
+ timestamp=datetime.fromisoformat(msg_data["timestamp"]),
145
+ model_used=msg_data["model_used"],
146
+ )
147
+ messages.append(message)
148
+
149
+ # Create ChatContext
150
+ context = ChatContext(
151
+ messages=messages,
152
+ current_model=conversation_data.get("current_model", ""),
153
+ session_id=conversation_data.get("session_id", ""),
154
+ max_context_length=conversation_data.get("max_context_length", 4000),
155
+ )
156
+
157
+ return context
158
+
159
+ except ConversationError:
160
+ raise
161
+ except Exception as e:
162
+ raise ConversationError(f"Failed to load conversation: {e}")
163
+
164
+ def list_conversations(self) -> List[Dict[str, str]]:
165
+ """
166
+ List all available conversation files.
167
+
168
+ Returns:
169
+ List of conversation info dictionaries
170
+ """
171
+ conversations = []
172
+
173
+ try:
174
+ for file_path in self.conversations_dir.glob("*.json"):
175
+ try:
176
+ # Get basic file info
177
+ stat = file_path.stat()
178
+
179
+ # Try to get conversation metadata
180
+ with open(file_path, "r", encoding="utf-8") as f:
181
+ data = json.load(f)
182
+
183
+ conversations.append(
184
+ {
185
+ "filename": file_path.name,
186
+ "created_at": data.get("created_at", ""),
187
+ "session_id": data.get("session_id", ""),
188
+ "current_model": data.get("current_model", ""),
189
+ "message_count": len(data.get("messages", [])),
190
+ "file_size": stat.st_size,
191
+ "modified_at": datetime.fromtimestamp(
192
+ stat.st_mtime
193
+ ).isoformat(),
194
+ }
195
+ )
196
+
197
+ except Exception:
198
+ # Skip files that can't be read
199
+ continue
200
+
201
+ # Sort by creation date (newest first)
202
+ conversations.sort(key=lambda x: x.get("created_at", ""), reverse=True)
203
+
204
+ except Exception:
205
+ # Return empty list if directory can't be read
206
+ pass
207
+
208
+ return conversations
209
+
210
+ def delete_conversation(self, filename: str) -> bool:
211
+ """
212
+ Delete a conversation file.
213
+
214
+ Args:
215
+ filename: Name of the conversation file
216
+
217
+ Returns:
218
+ True if deleted successfully, False otherwise
219
+ """
220
+ try:
221
+ # Ensure .json extension
222
+ if not filename.endswith(".json"):
223
+ filename += ".json"
224
+
225
+ file_path = self.conversations_dir / filename
226
+
227
+ if file_path.exists():
228
+ file_path.unlink()
229
+ return True
230
+
231
+ return False
232
+
233
+ except Exception:
234
+ return False
235
+
236
+ def get_conversation_info(self, filename: str) -> Optional[Dict]:
237
+ """
238
+ Get information about a specific conversation.
239
+
240
+ Args:
241
+ filename: Name of the conversation file
242
+
243
+ Returns:
244
+ Conversation info dictionary or None if not found
245
+ """
246
+ try:
247
+ # Ensure .json extension
248
+ if not filename.endswith(".json"):
249
+ filename += ".json"
250
+
251
+ file_path = self.conversations_dir / filename
252
+
253
+ if not file_path.exists():
254
+ return None
255
+
256
+ with open(file_path, "r", encoding="utf-8") as f:
257
+ data = json.load(f)
258
+
259
+ stat = file_path.stat()
260
+
261
+ return {
262
+ "filename": filename,
263
+ "created_at": data.get("created_at", ""),
264
+ "session_id": data.get("session_id", ""),
265
+ "current_model": data.get("current_model", ""),
266
+ "message_count": len(data.get("messages", [])),
267
+ "file_size": stat.st_size,
268
+ "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(),
269
+ "metadata": data.get("metadata", {}),
270
+ }
271
+
272
+ except Exception:
273
+ return None
274
+
275
+ def export_conversation(
276
+ self, filename: str, export_format: str = "json"
277
+ ) -> Optional[str]:
278
+ """
279
+ Export a conversation to different formats.
280
+
281
+ Args:
282
+ filename: Name of the conversation file
283
+ export_format: Export format ("json", "txt", "md")
284
+
285
+ Returns:
286
+ Path to exported file or None if failed
287
+ """
288
+ try:
289
+ context = self.load_conversation(filename)
290
+
291
+ base_name = filename.replace(".json", "")
292
+
293
+ if export_format == "txt":
294
+ return self._export_to_txt(context, base_name)
295
+ elif export_format == "md":
296
+ return self._export_to_markdown(context, base_name)
297
+ elif export_format == "json":
298
+ # Already in JSON format
299
+ return str(self.conversations_dir / filename)
300
+ else:
301
+ raise ConversationError(f"Unsupported export format: {export_format}")
302
+
303
+ except Exception as e:
304
+ raise ConversationError(f"Failed to export conversation: {e}")
305
+
306
+ def _export_to_txt(self, context: ChatContext, base_name: str) -> str:
307
+ """Export conversation to plain text format."""
308
+ export_path = self.conversations_dir / f"{base_name}.txt"
309
+
310
+ with open(export_path, "w", encoding="utf-8") as f:
311
+ f.write(f"Omnimancer Conversation Export\n")
312
+ f.write(f"Session ID: {context.session_id}\n")
313
+ f.write(f"Model: {context.current_model}\n")
314
+ f.write(f"Messages: {len(context.messages)}\n")
315
+ f.write("=" * 50 + "\n\n")
316
+
317
+ for msg in context.messages:
318
+ f.write(f"[{msg.timestamp.strftime('%Y-%m-%d %H:%M:%S')}] ")
319
+ f.write(f"{msg.role.value.upper()}")
320
+ if msg.model_used:
321
+ f.write(f" ({msg.model_used})")
322
+ f.write(":\n")
323
+ f.write(f"{msg.content}\n\n")
324
+
325
+ return str(export_path)
326
+
327
+ def _export_to_markdown(self, context: ChatContext, base_name: str) -> str:
328
+ """Export conversation to Markdown format."""
329
+ export_path = self.conversations_dir / f"{base_name}.md"
330
+
331
+ with open(export_path, "w", encoding="utf-8") as f:
332
+ f.write(f"# Omnimancer Conversation\n\n")
333
+ f.write(f"**Session ID:** {context.session_id}\n")
334
+ f.write(f"**Model:** {context.current_model}\n")
335
+ f.write(f"**Messages:** {len(context.messages)}\n\n")
336
+ f.write("---\n\n")
337
+
338
+ for msg in context.messages:
339
+ timestamp = msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")
340
+ role = msg.role.value.title()
341
+
342
+ if msg.role == MessageRole.USER:
343
+ f.write(f"## 👤 {role} ({timestamp})\n\n")
344
+ elif msg.role == MessageRole.ASSISTANT:
345
+ model_info = f" - {msg.model_used}" if msg.model_used else ""
346
+ f.write(f"## 🤖 {role}{model_info} ({timestamp})\n\n")
347
+ else:
348
+ f.write(f"## ⚙️ {role} ({timestamp})\n\n")
349
+
350
+ f.write(f"{msg.content}\n\n")
351
+
352
+ return str(export_path)