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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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)
|