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,462 @@
1
+ """
2
+ Command History Manager for Omnimancer CLI.
3
+
4
+ This module provides command history functionality with persistent storage,
5
+ search capabilities, and arrow key navigation similar to claude-code.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import time
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class HistoryEntry:
20
+ """Represents a single command history entry."""
21
+
22
+ def __init__(
23
+ self,
24
+ command: str,
25
+ timestamp: Optional[float] = None,
26
+ session_id: Optional[str] = None,
27
+ metadata: Optional[Dict[str, Any]] = None,
28
+ ):
29
+ """
30
+ Initialize a history entry.
31
+
32
+ Args:
33
+ command: The command text
34
+ timestamp: Unix timestamp (defaults to current time)
35
+ session_id: Optional session identifier
36
+ metadata: Additional metadata about the command
37
+ """
38
+ self.command = command.strip()
39
+ self.timestamp = timestamp or time.time()
40
+ self.session_id = session_id
41
+ self.metadata = metadata or {}
42
+
43
+ def to_dict(self) -> Dict[str, Any]:
44
+ """Convert to dictionary for JSON serialization."""
45
+ return {
46
+ "command": self.command,
47
+ "timestamp": self.timestamp,
48
+ "session_id": self.session_id,
49
+ "metadata": self.metadata,
50
+ }
51
+
52
+ @classmethod
53
+ def from_dict(cls, data: Dict[str, Any]) -> "HistoryEntry":
54
+ """Create from dictionary."""
55
+ return cls(
56
+ command=data["command"],
57
+ timestamp=data.get("timestamp"),
58
+ session_id=data.get("session_id"),
59
+ metadata=data.get("metadata", {}),
60
+ )
61
+
62
+ @property
63
+ def datetime(self) -> datetime:
64
+ """Get datetime representation of timestamp."""
65
+ return datetime.fromtimestamp(self.timestamp)
66
+
67
+ def __str__(self) -> str:
68
+ return self.command
69
+
70
+ def __repr__(self) -> str:
71
+ return f"HistoryEntry(command='{self.command[:50]}...', timestamp={self.timestamp})"
72
+
73
+
74
+ class HistoryManager:
75
+ """
76
+ Manages command history with persistent storage and search capabilities.
77
+
78
+ Features:
79
+ - Persistent storage in JSON format
80
+ - Configurable history size limits
81
+ - Search and filtering
82
+ - Session tracking
83
+ - Automatic cleanup of old entries
84
+ - Duplicate prevention
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ storage_path: str = "~/.omnimancer",
90
+ max_entries: int = 1000,
91
+ max_age_days: int = 90,
92
+ session_id: Optional[str] = None,
93
+ ):
94
+ """
95
+ Initialize the history manager.
96
+
97
+ Args:
98
+ storage_path: Directory for history storage
99
+ max_entries: Maximum number of history entries to keep
100
+ max_age_days: Maximum age in days before entries are cleaned up
101
+ session_id: Optional session identifier
102
+ """
103
+ self.storage_path = Path(storage_path).expanduser()
104
+ self.history_file = self.storage_path / "command_history.json"
105
+ self.max_entries = max_entries
106
+ self.max_age_days = max_age_days
107
+ self.session_id = session_id or self._generate_session_id()
108
+
109
+ # In-memory history for current session
110
+ self._history: List[HistoryEntry] = []
111
+ self._current_index = 0 # For arrow key navigation
112
+ self._search_filter = ""
113
+ self._filtered_history: List[HistoryEntry] = []
114
+
115
+ # Ensure storage directory exists
116
+ self.storage_path.mkdir(parents=True, exist_ok=True)
117
+
118
+ # Load existing history
119
+ self._load_history()
120
+
121
+ # Clean up old entries
122
+ self._cleanup_old_entries()
123
+
124
+ def _generate_session_id(self) -> str:
125
+ """Generate a unique session identifier."""
126
+ return f"session_{int(time.time())}_{os.getpid()}"
127
+
128
+ def add_command(
129
+ self, command: str, metadata: Optional[Dict[str, Any]] = None
130
+ ) -> None:
131
+ """
132
+ Add a command to history.
133
+
134
+ Args:
135
+ command: Command text to add
136
+ metadata: Optional metadata about the command
137
+ """
138
+ command = command.strip()
139
+
140
+ # Skip empty commands
141
+ if not command:
142
+ return
143
+
144
+ # Skip duplicate consecutive commands
145
+ if self._history and self._history[-1].command == command:
146
+ return
147
+
148
+ # Create history entry
149
+ entry = HistoryEntry(
150
+ command=command, session_id=self.session_id, metadata=metadata
151
+ )
152
+
153
+ # Add to in-memory history
154
+ self._history.append(entry)
155
+
156
+ # Reset navigation index
157
+ self._current_index = len(self._history)
158
+
159
+ # Trim history if it exceeds max size
160
+ if len(self._history) > self.max_entries:
161
+ self._history = self._history[-self.max_entries :]
162
+
163
+ # Save to disk (async would be better but keeping simple for now)
164
+ self._save_history()
165
+
166
+ logger.debug(f"Added command to history: {command[:50]}...")
167
+
168
+ def get_previous_command(self) -> Optional[str]:
169
+ """
170
+ Get the previous command in history (up arrow functionality).
171
+
172
+ Returns:
173
+ Previous command or None if at beginning
174
+ """
175
+ if not self._get_filtered_history():
176
+ return None
177
+
178
+ if self._current_index > 0:
179
+ self._current_index -= 1
180
+ return self._get_filtered_history()[self._current_index].command
181
+
182
+ return None
183
+
184
+ def get_next_command(self) -> Optional[str]:
185
+ """
186
+ Get the next command in history (down arrow functionality).
187
+
188
+ Returns:
189
+ Next command or None if at end
190
+ """
191
+ filtered = self._get_filtered_history()
192
+ if not filtered:
193
+ return None
194
+
195
+ if self._current_index < len(filtered) - 1:
196
+ self._current_index += 1
197
+ return filtered[self._current_index].command
198
+ else:
199
+ # Go to end (empty command)
200
+ self._current_index = len(filtered)
201
+ return ""
202
+
203
+ def search_history(self, query: str, limit: int = 50) -> List[HistoryEntry]:
204
+ """
205
+ Search command history.
206
+
207
+ Args:
208
+ query: Search query (case insensitive)
209
+ limit: Maximum number of results
210
+
211
+ Returns:
212
+ List of matching history entries
213
+ """
214
+ query = query.lower()
215
+ results = []
216
+
217
+ # Search in reverse order (most recent first)
218
+ for entry in reversed(self._history):
219
+ if query in entry.command.lower():
220
+ results.append(entry)
221
+ if len(results) >= limit:
222
+ break
223
+
224
+ return results
225
+
226
+ def set_search_filter(self, filter_text: str) -> None:
227
+ """
228
+ Set a search filter for arrow key navigation.
229
+
230
+ Args:
231
+ filter_text: Text to filter commands by
232
+ """
233
+ self._search_filter = filter_text.lower()
234
+ self._filtered_history = [] # Will be rebuilt on next access
235
+
236
+ # Reset navigation to end of filtered results
237
+ filtered = self._get_filtered_history()
238
+ self._current_index = len(filtered)
239
+
240
+ def clear_search_filter(self) -> None:
241
+ """Clear the current search filter."""
242
+ self._search_filter = ""
243
+ self._filtered_history = []
244
+ self._current_index = len(self._history)
245
+
246
+ def _get_filtered_history(self) -> List[HistoryEntry]:
247
+ """Get history filtered by current search filter."""
248
+ if not self._search_filter:
249
+ return self._history
250
+
251
+ # Rebuild filtered history if needed
252
+ if not self._filtered_history:
253
+ self._filtered_history = [
254
+ entry
255
+ for entry in self._history
256
+ if self._search_filter in entry.command.lower()
257
+ ]
258
+
259
+ return self._filtered_history
260
+
261
+ def get_recent_commands(self, limit: int = 20) -> List[HistoryEntry]:
262
+ """
263
+ Get recent commands.
264
+
265
+ Args:
266
+ limit: Maximum number of commands to return
267
+
268
+ Returns:
269
+ List of recent history entries
270
+ """
271
+ return self._history[-limit:] if self._history else []
272
+
273
+ def get_session_commands(
274
+ self, session_id: Optional[str] = None
275
+ ) -> List[HistoryEntry]:
276
+ """
277
+ Get commands from a specific session.
278
+
279
+ Args:
280
+ session_id: Session ID (defaults to current session)
281
+
282
+ Returns:
283
+ List of commands from the session
284
+ """
285
+ target_session = session_id or self.session_id
286
+ return [entry for entry in self._history if entry.session_id == target_session]
287
+
288
+ def get_statistics(self) -> Dict[str, Any]:
289
+ """
290
+ Get history statistics.
291
+
292
+ Returns:
293
+ Dictionary with history statistics
294
+ """
295
+ if not self._history:
296
+ return {
297
+ "total_commands": 0,
298
+ "current_session_commands": 0,
299
+ "oldest_entry": None,
300
+ "newest_entry": None,
301
+ "unique_commands": 0,
302
+ }
303
+
304
+ unique_commands = len(set(entry.command for entry in self._history))
305
+ current_session_count = len(self.get_session_commands())
306
+
307
+ return {
308
+ "total_commands": len(self._history),
309
+ "current_session_commands": current_session_count,
310
+ "oldest_entry": self._history[0].datetime.isoformat(),
311
+ "newest_entry": self._history[-1].datetime.isoformat(),
312
+ "unique_commands": unique_commands,
313
+ "sessions": len(
314
+ set(entry.session_id for entry in self._history if entry.session_id)
315
+ ),
316
+ }
317
+
318
+ def clear_history(self, confirm: bool = False) -> bool:
319
+ """
320
+ Clear all command history.
321
+
322
+ Args:
323
+ confirm: Must be True to actually clear
324
+
325
+ Returns:
326
+ True if history was cleared
327
+ """
328
+ if not confirm:
329
+ return False
330
+
331
+ self._history.clear()
332
+ self._current_index = 0
333
+ self._filtered_history.clear()
334
+
335
+ # Remove history file
336
+ if self.history_file.exists():
337
+ self.history_file.unlink()
338
+
339
+ logger.info("Command history cleared")
340
+ return True
341
+
342
+ def export_history(self, filepath: str, format: str = "json") -> bool:
343
+ """
344
+ Export history to a file.
345
+
346
+ Args:
347
+ filepath: Path to export file
348
+ format: Export format ('json' or 'txt')
349
+
350
+ Returns:
351
+ True if export was successful
352
+ """
353
+ try:
354
+ export_path = Path(filepath)
355
+
356
+ if format.lower() == "json":
357
+ data = {
358
+ "exported_at": datetime.now().isoformat(),
359
+ "session_id": self.session_id,
360
+ "history": [entry.to_dict() for entry in self._history],
361
+ }
362
+
363
+ with open(export_path, "w") as f:
364
+ json.dump(data, f, indent=2)
365
+
366
+ elif format.lower() == "txt":
367
+ with open(export_path, "w") as f:
368
+ f.write(f"# Omnimancer Command History Export\n")
369
+ f.write(f"# Exported at: {datetime.now().isoformat()}\n")
370
+ f.write(f"# Session ID: {self.session_id}\n\n")
371
+
372
+ for entry in self._history:
373
+ f.write(f"# {entry.datetime.strftime('%Y-%m-%d %H:%M:%S')}\n")
374
+ f.write(f"{entry.command}\n\n")
375
+
376
+ else:
377
+ raise ValueError(f"Unsupported export format: {format}")
378
+
379
+ logger.info(f"History exported to {filepath}")
380
+ return True
381
+
382
+ except Exception as e:
383
+ logger.error(f"Failed to export history: {e}")
384
+ return False
385
+
386
+ def _load_history(self) -> None:
387
+ """Load history from disk."""
388
+ try:
389
+ if not self.history_file.exists():
390
+ return
391
+
392
+ with open(self.history_file, "r") as f:
393
+ data = json.load(f)
394
+
395
+ # Handle different file formats
396
+ if isinstance(data, list):
397
+ # Old format - list of commands
398
+ for item in data:
399
+ if isinstance(item, str):
400
+ self._history.append(HistoryEntry(item))
401
+ elif isinstance(item, dict):
402
+ self._history.append(HistoryEntry.from_dict(item))
403
+
404
+ elif isinstance(data, dict) and "history" in data:
405
+ # New format - structured data
406
+ for entry_data in data["history"]:
407
+ self._history.append(HistoryEntry.from_dict(entry_data))
408
+
409
+ # Set navigation index to end
410
+ self._current_index = len(self._history)
411
+
412
+ logger.debug(f"Loaded {len(self._history)} history entries")
413
+
414
+ except Exception as e:
415
+ logger.error(f"Failed to load command history: {e}")
416
+ # Continue with empty history
417
+ self._history = []
418
+
419
+ def _save_history(self) -> None:
420
+ """Save history to disk."""
421
+ try:
422
+ data = {
423
+ "format_version": "1.0",
424
+ "saved_at": datetime.now().isoformat(),
425
+ "session_id": self.session_id,
426
+ "history": [entry.to_dict() for entry in self._history],
427
+ }
428
+
429
+ # Write atomically
430
+ temp_file = self.history_file.with_suffix(".tmp")
431
+ with open(temp_file, "w") as f:
432
+ json.dump(data, f, indent=2)
433
+
434
+ temp_file.replace(self.history_file)
435
+
436
+ except Exception as e:
437
+ logger.error(f"Failed to save command history: {e}")
438
+
439
+ def _cleanup_old_entries(self) -> None:
440
+ """Remove entries older than max_age_days."""
441
+ if not self._history or self.max_age_days <= 0:
442
+ return
443
+
444
+ cutoff_time = time.time() - (self.max_age_days * 24 * 60 * 60)
445
+ original_count = len(self._history)
446
+
447
+ self._history = [
448
+ entry for entry in self._history if entry.timestamp > cutoff_time
449
+ ]
450
+
451
+ removed_count = original_count - len(self._history)
452
+ if removed_count > 0:
453
+ logger.info(f"Cleaned up {removed_count} old history entries")
454
+ self._save_history()
455
+
456
+ def __len__(self) -> int:
457
+ """Return number of history entries."""
458
+ return len(self._history)
459
+
460
+ def __bool__(self) -> bool:
461
+ """Return True if history is not empty."""
462
+ return bool(self._history)