pomera-ai-commander 0.1.0 → 1.2.1

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 (191) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +105 -680
  3. package/bin/pomera-ai-commander.js +62 -62
  4. package/core/__init__.py +65 -65
  5. package/core/app_context.py +482 -482
  6. package/core/async_text_processor.py +421 -421
  7. package/core/backup_manager.py +655 -655
  8. package/core/backup_recovery_manager.py +1033 -1033
  9. package/core/content_hash_cache.py +508 -508
  10. package/core/context_menu.py +313 -313
  11. package/core/data_validator.py +1066 -1066
  12. package/core/database_connection_manager.py +744 -744
  13. package/core/database_curl_settings_manager.py +608 -608
  14. package/core/database_promera_ai_settings_manager.py +446 -446
  15. package/core/database_schema.py +411 -411
  16. package/core/database_schema_manager.py +395 -395
  17. package/core/database_settings_manager.py +1507 -1507
  18. package/core/database_settings_manager_interface.py +456 -456
  19. package/core/dialog_manager.py +734 -734
  20. package/core/efficient_line_numbers.py +510 -510
  21. package/core/error_handler.py +746 -746
  22. package/core/error_service.py +431 -431
  23. package/core/event_consolidator.py +511 -511
  24. package/core/mcp/__init__.py +43 -43
  25. package/core/mcp/protocol.py +288 -288
  26. package/core/mcp/schema.py +251 -251
  27. package/core/mcp/server_stdio.py +299 -299
  28. package/core/mcp/tool_registry.py +2372 -2345
  29. package/core/memory_efficient_text_widget.py +711 -711
  30. package/core/migration_manager.py +914 -914
  31. package/core/migration_test_suite.py +1085 -1085
  32. package/core/migration_validator.py +1143 -1143
  33. package/core/optimized_find_replace.py +714 -714
  34. package/core/optimized_pattern_engine.py +424 -424
  35. package/core/optimized_search_highlighter.py +552 -552
  36. package/core/performance_monitor.py +674 -674
  37. package/core/persistence_manager.py +712 -712
  38. package/core/progressive_stats_calculator.py +632 -632
  39. package/core/regex_pattern_cache.py +529 -529
  40. package/core/regex_pattern_library.py +350 -350
  41. package/core/search_operation_manager.py +434 -434
  42. package/core/settings_defaults_registry.py +1087 -1087
  43. package/core/settings_integrity_validator.py +1111 -1111
  44. package/core/settings_serializer.py +557 -557
  45. package/core/settings_validator.py +1823 -1823
  46. package/core/smart_stats_calculator.py +709 -709
  47. package/core/statistics_update_manager.py +619 -619
  48. package/core/stats_config_manager.py +858 -858
  49. package/core/streaming_text_handler.py +723 -723
  50. package/core/task_scheduler.py +596 -596
  51. package/core/update_pattern_library.py +168 -168
  52. package/core/visibility_monitor.py +596 -596
  53. package/core/widget_cache.py +498 -498
  54. package/mcp.json +51 -61
  55. package/package.json +61 -57
  56. package/pomera.py +7482 -7482
  57. package/pomera_mcp_server.py +183 -144
  58. package/requirements.txt +32 -0
  59. package/tools/__init__.py +4 -4
  60. package/tools/ai_tools.py +2891 -2891
  61. package/tools/ascii_art_generator.py +352 -352
  62. package/tools/base64_tools.py +183 -183
  63. package/tools/base_tool.py +511 -511
  64. package/tools/case_tool.py +308 -308
  65. package/tools/column_tools.py +395 -395
  66. package/tools/cron_tool.py +884 -884
  67. package/tools/curl_history.py +600 -600
  68. package/tools/curl_processor.py +1207 -1207
  69. package/tools/curl_settings.py +502 -502
  70. package/tools/curl_tool.py +5467 -5467
  71. package/tools/diff_viewer.py +1071 -1071
  72. package/tools/email_extraction_tool.py +248 -248
  73. package/tools/email_header_analyzer.py +425 -425
  74. package/tools/extraction_tools.py +250 -250
  75. package/tools/find_replace.py +1750 -1750
  76. package/tools/folder_file_reporter.py +1463 -1463
  77. package/tools/folder_file_reporter_adapter.py +480 -480
  78. package/tools/generator_tools.py +1216 -1216
  79. package/tools/hash_generator.py +255 -255
  80. package/tools/html_tool.py +656 -656
  81. package/tools/jsonxml_tool.py +729 -729
  82. package/tools/line_tools.py +419 -419
  83. package/tools/markdown_tools.py +561 -561
  84. package/tools/mcp_widget.py +1417 -1417
  85. package/tools/notes_widget.py +973 -973
  86. package/tools/number_base_converter.py +372 -372
  87. package/tools/regex_extractor.py +571 -571
  88. package/tools/slug_generator.py +310 -310
  89. package/tools/sorter_tools.py +458 -458
  90. package/tools/string_escape_tool.py +392 -392
  91. package/tools/text_statistics_tool.py +365 -365
  92. package/tools/text_wrapper.py +430 -430
  93. package/tools/timestamp_converter.py +421 -421
  94. package/tools/tool_loader.py +710 -710
  95. package/tools/translator_tools.py +522 -522
  96. package/tools/url_link_extractor.py +261 -261
  97. package/tools/url_parser.py +204 -204
  98. package/tools/whitespace_tools.py +355 -355
  99. package/tools/word_frequency_counter.py +146 -146
  100. package/core/__pycache__/__init__.cpython-313.pyc +0 -0
  101. package/core/__pycache__/app_context.cpython-313.pyc +0 -0
  102. package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
  103. package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
  104. package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
  105. package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
  106. package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
  107. package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
  108. package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
  109. package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
  110. package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
  111. package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
  112. package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
  113. package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
  114. package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
  115. package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
  116. package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
  117. package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
  118. package/core/__pycache__/error_service.cpython-313.pyc +0 -0
  119. package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
  120. package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
  121. package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
  122. package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
  123. package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
  124. package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
  125. package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
  126. package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
  127. package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
  128. package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
  129. package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
  130. package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
  131. package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
  132. package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
  133. package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
  134. package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
  135. package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
  136. package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
  137. package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
  138. package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
  139. package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
  140. package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
  141. package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
  142. package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
  143. package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
  144. package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
  145. package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
  146. package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
  147. package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
  148. package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
  149. package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
  151. package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
  152. package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
  153. package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
  154. package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
  155. package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
  156. package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
  157. package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
  158. package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
  159. package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
  160. package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
  161. package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
  162. package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
  163. package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
  164. package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
  165. package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
  166. package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
  167. package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
  168. package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
  169. package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
  170. package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
  171. package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
  172. package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
  173. package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
  174. package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
  175. package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
  176. package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
  177. package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
  178. package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
  179. package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
  180. package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
  181. package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
  182. package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
  183. package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
  184. package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
  185. package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
  186. package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
  187. package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
  188. package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
  189. package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
  190. package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
  191. package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
@@ -1,601 +1,601 @@
1
- """
2
- cURL Tool History Management Module
3
-
4
- This module provides request history management for the cURL GUI Tool.
5
- It handles history storage, persistence, and organization functionality.
6
-
7
- Author: Pomera AI Commander
8
- """
9
-
10
- import json
11
- import os
12
- from typing import Dict, Any, Optional, List
13
- import logging
14
- from datetime import datetime, timedelta
15
- from dataclasses import dataclass, asdict
16
-
17
-
18
- @dataclass
19
- class RequestHistoryItem:
20
- """Individual request history entry."""
21
- timestamp: str
22
- method: str
23
- url: str
24
- status_code: Optional[int]
25
- response_time: Optional[float]
26
- success: bool
27
- headers: Dict[str, str]
28
- body: Optional[str]
29
- auth_type: str
30
- response_preview: str # First 200 chars of response
31
- response_size: Optional[int]
32
- content_type: Optional[str]
33
-
34
- def to_dict(self) -> Dict[str, Any]:
35
- """Convert to dictionary for JSON serialization."""
36
- return asdict(self)
37
-
38
- @classmethod
39
- def from_dict(cls, data: Dict[str, Any]) -> 'RequestHistoryItem':
40
- """Create from dictionary."""
41
- return cls(**data)
42
-
43
-
44
- class CurlHistoryManager:
45
- """
46
- Manages request history persistence and organization for the cURL Tool.
47
-
48
- Handles:
49
- - History file storage and loading
50
- - History item management (add, remove, search)
51
- - History cleanup and organization
52
- - Collections support
53
- """
54
-
55
- def __init__(self, history_file: str = "settings.json",
56
- max_items: int = 100, logger=None):
57
- """
58
- Initialize the history manager.
59
-
60
- Args:
61
- history_file: Path to the main settings file
62
- max_items: Maximum number of history items to keep
63
- logger: Logger instance for debugging
64
- """
65
- self.history_file = history_file
66
- self.max_items = max_items
67
- self.logger = logger or logging.getLogger(__name__)
68
- self.tool_key = "cURL Tool" # Key in tool_settings section
69
-
70
- # History storage
71
- self.history: List[RequestHistoryItem] = []
72
- self.collections: Dict[str, List[str]] = {} # Collection name -> list of history item IDs
73
-
74
- # Load history on initialization
75
- self.load_history()
76
-
77
- def add_request(self, method: str, url: str, headers: Dict[str, str] = None,
78
- body: str = None, auth_type: str = "None",
79
- status_code: int = None, response_time: float = None,
80
- success: bool = True, response_body: str = "",
81
- response_size: int = None, content_type: str = None) -> str:
82
- """
83
- Add a request to history.
84
-
85
- Args:
86
- method: HTTP method
87
- url: Request URL
88
- headers: Request headers
89
- body: Request body
90
- auth_type: Authentication type used
91
- status_code: Response status code
92
- response_time: Response time in seconds
93
- success: Whether request was successful
94
- response_body: Response body content
95
- response_size: Response size in bytes
96
- content_type: Response content type
97
-
98
- Returns:
99
- History item ID (timestamp)
100
- """
101
- timestamp = datetime.now().isoformat()
102
-
103
- # Create response preview (first 200 chars)
104
- response_preview = ""
105
- if response_body:
106
- response_preview = response_body[:200]
107
- if len(response_body) > 200:
108
- response_preview += "..."
109
-
110
- # Create history item
111
- history_item = RequestHistoryItem(
112
- timestamp=timestamp,
113
- method=method,
114
- url=url,
115
- status_code=status_code,
116
- response_time=response_time,
117
- success=success,
118
- headers=headers or {},
119
- body=body,
120
- auth_type=auth_type,
121
- response_preview=response_preview,
122
- response_size=response_size,
123
- content_type=content_type
124
- )
125
-
126
- # Add to history
127
- self.history.append(history_item)
128
-
129
- # Maintain max items limit
130
- if len(self.history) > self.max_items:
131
- removed_item = self.history.pop(0)
132
- # Remove from collections if present
133
- self._remove_from_collections(removed_item.timestamp)
134
-
135
- # Save history
136
- self.save_history()
137
-
138
- self.logger.debug(f"Added request to history: {method} {url}")
139
- return timestamp
140
-
141
- def get_history(self, limit: int = None) -> List[RequestHistoryItem]:
142
- """
143
- Get history items.
144
-
145
- Args:
146
- limit: Maximum number of items to return
147
-
148
- Returns:
149
- List of history items (newest first)
150
- """
151
- history = sorted(self.history, key=lambda x: x.timestamp, reverse=True)
152
- if limit:
153
- return history[:limit]
154
- return history
155
-
156
- def get_history_item(self, timestamp: str) -> Optional[RequestHistoryItem]:
157
- """
158
- Get a specific history item by timestamp.
159
-
160
- Args:
161
- timestamp: Item timestamp ID
162
-
163
- Returns:
164
- History item or None if not found
165
- """
166
- for item in self.history:
167
- if item.timestamp == timestamp:
168
- return item
169
- return None
170
-
171
- def remove_history_item(self, timestamp: str) -> bool:
172
- """
173
- Remove a history item.
174
-
175
- Args:
176
- timestamp: Item timestamp ID
177
-
178
- Returns:
179
- True if removed, False if not found
180
- """
181
- for i, item in enumerate(self.history):
182
- if item.timestamp == timestamp:
183
- self.history.pop(i)
184
- self._remove_from_collections(timestamp)
185
- self.save_history()
186
- self.logger.debug(f"Removed history item: {timestamp}")
187
- return True
188
- return False
189
-
190
- def clear_history(self) -> bool:
191
- """
192
- Clear all history.
193
-
194
- Returns:
195
- True if successful
196
- """
197
- try:
198
- self.history.clear()
199
- self.collections.clear()
200
- self.save_history()
201
- self.logger.info("History cleared")
202
- return True
203
- except Exception as e:
204
- self.logger.error(f"Error clearing history: {e}")
205
- return False
206
-
207
- def search_history(self, query: str, field: str = "all") -> List[RequestHistoryItem]:
208
- """
209
- Search history items.
210
-
211
- Args:
212
- query: Search query
213
- field: Field to search in ("all", "url", "method", "status")
214
-
215
- Returns:
216
- List of matching history items
217
- """
218
- query = query.lower()
219
- results = []
220
-
221
- for item in self.history:
222
- match = False
223
-
224
- if field == "all":
225
- # Search in multiple fields
226
- search_text = f"{item.method} {item.url} {item.status_code or ''} {item.response_preview}".lower()
227
- match = query in search_text
228
- elif field == "url":
229
- match = query in item.url.lower()
230
- elif field == "method":
231
- match = query in item.method.lower()
232
- elif field == "status":
233
- match = query in str(item.status_code or "")
234
-
235
- if match:
236
- results.append(item)
237
-
238
- return sorted(results, key=lambda x: x.timestamp, reverse=True)
239
-
240
- def cleanup_old_history(self, retention_days: int = 30) -> int:
241
- """
242
- Clean up old history items.
243
-
244
- Args:
245
- retention_days: Number of days to retain history
246
-
247
- Returns:
248
- Number of items removed
249
- """
250
- try:
251
- cutoff_date = datetime.now() - timedelta(days=retention_days)
252
- initial_count = len(self.history)
253
-
254
- # Filter out old items
255
- self.history = [
256
- item for item in self.history
257
- if datetime.fromisoformat(item.timestamp) > cutoff_date
258
- ]
259
-
260
- removed_count = initial_count - len(self.history)
261
-
262
- if removed_count > 0:
263
- # Clean up collections
264
- self._cleanup_collections()
265
- self.save_history()
266
- self.logger.info(f"Cleaned up {removed_count} old history items")
267
-
268
- return removed_count
269
-
270
- except Exception as e:
271
- self.logger.error(f"Error cleaning up history: {e}")
272
- return 0
273
-
274
- def create_collection(self, name: str, item_timestamps: List[str] = None) -> bool:
275
- """
276
- Create a new collection.
277
-
278
- Args:
279
- name: Collection name
280
- item_timestamps: List of history item timestamps to include
281
-
282
- Returns:
283
- True if successful
284
- """
285
- try:
286
- if name in self.collections:
287
- self.logger.warning(f"Collection '{name}' already exists")
288
- return False
289
-
290
- self.collections[name] = item_timestamps or []
291
- self.save_history()
292
- self.logger.info(f"Created collection: {name}")
293
- return True
294
-
295
- except Exception as e:
296
- self.logger.error(f"Error creating collection: {e}")
297
- return False
298
-
299
- def add_to_collection(self, collection_name: str, timestamp: str) -> bool:
300
- """
301
- Add a history item to a collection.
302
-
303
- Args:
304
- collection_name: Name of the collection
305
- timestamp: History item timestamp
306
-
307
- Returns:
308
- True if successful
309
- """
310
- try:
311
- if collection_name not in self.collections:
312
- self.collections[collection_name] = []
313
-
314
- if timestamp not in self.collections[collection_name]:
315
- # Verify the history item exists
316
- if self.get_history_item(timestamp):
317
- self.collections[collection_name].append(timestamp)
318
- self.save_history()
319
- self.logger.debug(f"Added item {timestamp} to collection {collection_name}")
320
- return True
321
- else:
322
- self.logger.warning(f"History item {timestamp} not found")
323
- return False
324
- else:
325
- self.logger.debug(f"Item {timestamp} already in collection {collection_name}")
326
- return True
327
-
328
- except Exception as e:
329
- self.logger.error(f"Error adding to collection: {e}")
330
- return False
331
-
332
- def remove_from_collection(self, collection_name: str, timestamp: str) -> bool:
333
- """
334
- Remove a history item from a collection.
335
-
336
- Args:
337
- collection_name: Name of the collection
338
- timestamp: History item timestamp
339
-
340
- Returns:
341
- True if successful
342
- """
343
- try:
344
- if collection_name in self.collections:
345
- if timestamp in self.collections[collection_name]:
346
- self.collections[collection_name].remove(timestamp)
347
- self.save_history()
348
- self.logger.debug(f"Removed item {timestamp} from collection {collection_name}")
349
- return True
350
- return False
351
-
352
- except Exception as e:
353
- self.logger.error(f"Error removing from collection: {e}")
354
- return False
355
-
356
- def get_collection(self, name: str) -> List[RequestHistoryItem]:
357
- """
358
- Get items in a collection.
359
-
360
- Args:
361
- name: Collection name
362
-
363
- Returns:
364
- List of history items in the collection
365
- """
366
- if name not in self.collections:
367
- return []
368
-
369
- items = []
370
- for timestamp in self.collections[name]:
371
- item = self.get_history_item(timestamp)
372
- if item:
373
- items.append(item)
374
-
375
- return sorted(items, key=lambda x: x.timestamp, reverse=True)
376
-
377
- def get_collections(self) -> Dict[str, int]:
378
- """
379
- Get all collections with item counts.
380
-
381
- Returns:
382
- Dictionary of collection name -> item count
383
- """
384
- result = {}
385
- for name, timestamps in self.collections.items():
386
- # Count only existing items
387
- count = sum(1 for ts in timestamps if self.get_history_item(ts))
388
- result[name] = count
389
- return result
390
-
391
- def delete_collection(self, name: str) -> bool:
392
- """
393
- Delete a collection.
394
-
395
- Args:
396
- name: Collection name
397
-
398
- Returns:
399
- True if successful
400
- """
401
- try:
402
- if name in self.collections:
403
- del self.collections[name]
404
- self.save_history()
405
- self.logger.info(f"Deleted collection: {name}")
406
- return True
407
- return False
408
-
409
- except Exception as e:
410
- self.logger.error(f"Error deleting collection: {e}")
411
- return False
412
-
413
- def export_history(self, filepath: str, collection_name: str = None) -> bool:
414
- """
415
- Export history to a file.
416
-
417
- Args:
418
- filepath: Export file path
419
- collection_name: Optional collection name to export
420
-
421
- Returns:
422
- True if successful
423
- """
424
- try:
425
- if collection_name:
426
- items = self.get_collection(collection_name)
427
- export_data = {
428
- "type": "collection",
429
- "name": collection_name,
430
- "items": [item.to_dict() for item in items]
431
- }
432
- else:
433
- export_data = {
434
- "type": "full_history",
435
- "items": [item.to_dict() for item in self.history],
436
- "collections": self.collections
437
- }
438
-
439
- export_data["export_date"] = datetime.now().isoformat()
440
- export_data["version"] = "1.0"
441
-
442
- with open(filepath, 'w', encoding='utf-8') as f:
443
- json.dump(export_data, f, indent=2, ensure_ascii=False)
444
-
445
- self.logger.info(f"History exported to {filepath}")
446
- return True
447
-
448
- except Exception as e:
449
- self.logger.error(f"Error exporting history: {e}")
450
- return False
451
-
452
- def import_history(self, filepath: str, merge: bool = True) -> bool:
453
- """
454
- Import history from a file.
455
-
456
- Args:
457
- filepath: Import file path
458
- merge: Whether to merge with existing history
459
-
460
- Returns:
461
- True if successful
462
- """
463
- try:
464
- with open(filepath, 'r', encoding='utf-8') as f:
465
- import_data = json.load(f)
466
-
467
- if not merge:
468
- self.history.clear()
469
- self.collections.clear()
470
-
471
- # Import items
472
- items_data = import_data.get("items", [])
473
- for item_data in items_data:
474
- try:
475
- item = RequestHistoryItem.from_dict(item_data)
476
- # Check if item already exists (by timestamp)
477
- if not any(h.timestamp == item.timestamp for h in self.history):
478
- self.history.append(item)
479
- except Exception as e:
480
- self.logger.warning(f"Error importing history item: {e}")
481
-
482
- # Import collections if present
483
- if "collections" in import_data:
484
- for name, timestamps in import_data["collections"].items():
485
- if name not in self.collections:
486
- self.collections[name] = []
487
- # Add timestamps that don't already exist
488
- for ts in timestamps:
489
- if ts not in self.collections[name]:
490
- self.collections[name].append(ts)
491
-
492
- # Maintain max items limit
493
- if len(self.history) > self.max_items:
494
- # Sort by timestamp and keep newest
495
- self.history.sort(key=lambda x: x.timestamp, reverse=True)
496
- self.history = self.history[:self.max_items]
497
-
498
- self.save_history()
499
- self.logger.info(f"History imported from {filepath}")
500
- return True
501
-
502
- except Exception as e:
503
- self.logger.error(f"Error importing history: {e}")
504
- return False
505
-
506
- def load_history(self) -> bool:
507
- """
508
- Load history from centralized settings.json file.
509
-
510
- Returns:
511
- True if successful
512
- """
513
- try:
514
- if os.path.exists(self.history_file):
515
- with open(self.history_file, 'r', encoding='utf-8') as f:
516
- all_settings = json.load(f)
517
-
518
- # Get tool settings from tool_settings section
519
- tool_settings = all_settings.get("tool_settings", {})
520
- curl_settings = tool_settings.get(self.tool_key, {})
521
-
522
- # Load history items
523
- self.history = []
524
- for item_data in curl_settings.get("history", []):
525
- try:
526
- item = RequestHistoryItem.from_dict(item_data)
527
- self.history.append(item)
528
- except Exception as e:
529
- self.logger.warning(f"Error loading history item: {e}")
530
-
531
- # Load collections
532
- self.collections = curl_settings.get("collections", {})
533
-
534
- self.logger.info(f"Loaded {len(self.history)} history items from {self.history_file}")
535
- return True
536
- else:
537
- self.logger.info("No history file found, starting with empty history")
538
- return True
539
-
540
- except Exception as e:
541
- self.logger.error(f"Error loading history: {e}")
542
- return False
543
-
544
- def save_history(self) -> bool:
545
- """
546
- Save history to centralized settings.json file.
547
-
548
- Returns:
549
- True if successful
550
- """
551
- try:
552
- # Load existing settings file
553
- all_settings = {}
554
- if os.path.exists(self.history_file):
555
- with open(self.history_file, 'r', encoding='utf-8') as f:
556
- all_settings = json.load(f)
557
-
558
- # Ensure tool_settings section exists
559
- if "tool_settings" not in all_settings:
560
- all_settings["tool_settings"] = {}
561
-
562
- # Ensure cURL Tool section exists
563
- if self.tool_key not in all_settings["tool_settings"]:
564
- all_settings["tool_settings"][self.tool_key] = {}
565
-
566
- # Update history and collections in cURL Tool settings
567
- all_settings["tool_settings"][self.tool_key]["history"] = [item.to_dict() for item in self.history]
568
- all_settings["tool_settings"][self.tool_key]["collections"] = self.collections
569
- all_settings["tool_settings"][self.tool_key]["history_last_updated"] = datetime.now().isoformat()
570
- all_settings["tool_settings"][self.tool_key]["history_version"] = "1.0"
571
-
572
- # Write settings to file
573
- with open(self.history_file, 'w', encoding='utf-8') as f:
574
- json.dump(all_settings, f, indent=4, ensure_ascii=False)
575
-
576
- self.logger.debug(f"History saved to {self.history_file}")
577
- return True
578
-
579
- except Exception as e:
580
- self.logger.error(f"Error saving history: {e}")
581
- return False
582
-
583
- def _remove_from_collections(self, timestamp: str):
584
- """Remove a timestamp from all collections."""
585
- for collection_name in list(self.collections.keys()):
586
- if timestamp in self.collections[collection_name]:
587
- self.collections[collection_name].remove(timestamp)
588
-
589
- def _cleanup_collections(self):
590
- """Clean up collections by removing references to non-existent items."""
591
- for collection_name in list(self.collections.keys()):
592
- # Filter out timestamps that don't exist in history
593
- valid_timestamps = [
594
- ts for ts in self.collections[collection_name]
595
- if self.get_history_item(ts) is not None
596
- ]
597
- self.collections[collection_name] = valid_timestamps
598
-
599
- # Remove empty collections
600
- if not self.collections[collection_name]:
1
+ """
2
+ cURL Tool History Management Module
3
+
4
+ This module provides request history management for the cURL GUI Tool.
5
+ It handles history storage, persistence, and organization functionality.
6
+
7
+ Author: Pomera AI Commander
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from typing import Dict, Any, Optional, List
13
+ import logging
14
+ from datetime import datetime, timedelta
15
+ from dataclasses import dataclass, asdict
16
+
17
+
18
+ @dataclass
19
+ class RequestHistoryItem:
20
+ """Individual request history entry."""
21
+ timestamp: str
22
+ method: str
23
+ url: str
24
+ status_code: Optional[int]
25
+ response_time: Optional[float]
26
+ success: bool
27
+ headers: Dict[str, str]
28
+ body: Optional[str]
29
+ auth_type: str
30
+ response_preview: str # First 200 chars of response
31
+ response_size: Optional[int]
32
+ content_type: Optional[str]
33
+
34
+ def to_dict(self) -> Dict[str, Any]:
35
+ """Convert to dictionary for JSON serialization."""
36
+ return asdict(self)
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: Dict[str, Any]) -> 'RequestHistoryItem':
40
+ """Create from dictionary."""
41
+ return cls(**data)
42
+
43
+
44
+ class CurlHistoryManager:
45
+ """
46
+ Manages request history persistence and organization for the cURL Tool.
47
+
48
+ Handles:
49
+ - History file storage and loading
50
+ - History item management (add, remove, search)
51
+ - History cleanup and organization
52
+ - Collections support
53
+ """
54
+
55
+ def __init__(self, history_file: str = "settings.json",
56
+ max_items: int = 100, logger=None):
57
+ """
58
+ Initialize the history manager.
59
+
60
+ Args:
61
+ history_file: Path to the main settings file
62
+ max_items: Maximum number of history items to keep
63
+ logger: Logger instance for debugging
64
+ """
65
+ self.history_file = history_file
66
+ self.max_items = max_items
67
+ self.logger = logger or logging.getLogger(__name__)
68
+ self.tool_key = "cURL Tool" # Key in tool_settings section
69
+
70
+ # History storage
71
+ self.history: List[RequestHistoryItem] = []
72
+ self.collections: Dict[str, List[str]] = {} # Collection name -> list of history item IDs
73
+
74
+ # Load history on initialization
75
+ self.load_history()
76
+
77
+ def add_request(self, method: str, url: str, headers: Dict[str, str] = None,
78
+ body: str = None, auth_type: str = "None",
79
+ status_code: int = None, response_time: float = None,
80
+ success: bool = True, response_body: str = "",
81
+ response_size: int = None, content_type: str = None) -> str:
82
+ """
83
+ Add a request to history.
84
+
85
+ Args:
86
+ method: HTTP method
87
+ url: Request URL
88
+ headers: Request headers
89
+ body: Request body
90
+ auth_type: Authentication type used
91
+ status_code: Response status code
92
+ response_time: Response time in seconds
93
+ success: Whether request was successful
94
+ response_body: Response body content
95
+ response_size: Response size in bytes
96
+ content_type: Response content type
97
+
98
+ Returns:
99
+ History item ID (timestamp)
100
+ """
101
+ timestamp = datetime.now().isoformat()
102
+
103
+ # Create response preview (first 200 chars)
104
+ response_preview = ""
105
+ if response_body:
106
+ response_preview = response_body[:200]
107
+ if len(response_body) > 200:
108
+ response_preview += "..."
109
+
110
+ # Create history item
111
+ history_item = RequestHistoryItem(
112
+ timestamp=timestamp,
113
+ method=method,
114
+ url=url,
115
+ status_code=status_code,
116
+ response_time=response_time,
117
+ success=success,
118
+ headers=headers or {},
119
+ body=body,
120
+ auth_type=auth_type,
121
+ response_preview=response_preview,
122
+ response_size=response_size,
123
+ content_type=content_type
124
+ )
125
+
126
+ # Add to history
127
+ self.history.append(history_item)
128
+
129
+ # Maintain max items limit
130
+ if len(self.history) > self.max_items:
131
+ removed_item = self.history.pop(0)
132
+ # Remove from collections if present
133
+ self._remove_from_collections(removed_item.timestamp)
134
+
135
+ # Save history
136
+ self.save_history()
137
+
138
+ self.logger.debug(f"Added request to history: {method} {url}")
139
+ return timestamp
140
+
141
+ def get_history(self, limit: int = None) -> List[RequestHistoryItem]:
142
+ """
143
+ Get history items.
144
+
145
+ Args:
146
+ limit: Maximum number of items to return
147
+
148
+ Returns:
149
+ List of history items (newest first)
150
+ """
151
+ history = sorted(self.history, key=lambda x: x.timestamp, reverse=True)
152
+ if limit:
153
+ return history[:limit]
154
+ return history
155
+
156
+ def get_history_item(self, timestamp: str) -> Optional[RequestHistoryItem]:
157
+ """
158
+ Get a specific history item by timestamp.
159
+
160
+ Args:
161
+ timestamp: Item timestamp ID
162
+
163
+ Returns:
164
+ History item or None if not found
165
+ """
166
+ for item in self.history:
167
+ if item.timestamp == timestamp:
168
+ return item
169
+ return None
170
+
171
+ def remove_history_item(self, timestamp: str) -> bool:
172
+ """
173
+ Remove a history item.
174
+
175
+ Args:
176
+ timestamp: Item timestamp ID
177
+
178
+ Returns:
179
+ True if removed, False if not found
180
+ """
181
+ for i, item in enumerate(self.history):
182
+ if item.timestamp == timestamp:
183
+ self.history.pop(i)
184
+ self._remove_from_collections(timestamp)
185
+ self.save_history()
186
+ self.logger.debug(f"Removed history item: {timestamp}")
187
+ return True
188
+ return False
189
+
190
+ def clear_history(self) -> bool:
191
+ """
192
+ Clear all history.
193
+
194
+ Returns:
195
+ True if successful
196
+ """
197
+ try:
198
+ self.history.clear()
199
+ self.collections.clear()
200
+ self.save_history()
201
+ self.logger.info("History cleared")
202
+ return True
203
+ except Exception as e:
204
+ self.logger.error(f"Error clearing history: {e}")
205
+ return False
206
+
207
+ def search_history(self, query: str, field: str = "all") -> List[RequestHistoryItem]:
208
+ """
209
+ Search history items.
210
+
211
+ Args:
212
+ query: Search query
213
+ field: Field to search in ("all", "url", "method", "status")
214
+
215
+ Returns:
216
+ List of matching history items
217
+ """
218
+ query = query.lower()
219
+ results = []
220
+
221
+ for item in self.history:
222
+ match = False
223
+
224
+ if field == "all":
225
+ # Search in multiple fields
226
+ search_text = f"{item.method} {item.url} {item.status_code or ''} {item.response_preview}".lower()
227
+ match = query in search_text
228
+ elif field == "url":
229
+ match = query in item.url.lower()
230
+ elif field == "method":
231
+ match = query in item.method.lower()
232
+ elif field == "status":
233
+ match = query in str(item.status_code or "")
234
+
235
+ if match:
236
+ results.append(item)
237
+
238
+ return sorted(results, key=lambda x: x.timestamp, reverse=True)
239
+
240
+ def cleanup_old_history(self, retention_days: int = 30) -> int:
241
+ """
242
+ Clean up old history items.
243
+
244
+ Args:
245
+ retention_days: Number of days to retain history
246
+
247
+ Returns:
248
+ Number of items removed
249
+ """
250
+ try:
251
+ cutoff_date = datetime.now() - timedelta(days=retention_days)
252
+ initial_count = len(self.history)
253
+
254
+ # Filter out old items
255
+ self.history = [
256
+ item for item in self.history
257
+ if datetime.fromisoformat(item.timestamp) > cutoff_date
258
+ ]
259
+
260
+ removed_count = initial_count - len(self.history)
261
+
262
+ if removed_count > 0:
263
+ # Clean up collections
264
+ self._cleanup_collections()
265
+ self.save_history()
266
+ self.logger.info(f"Cleaned up {removed_count} old history items")
267
+
268
+ return removed_count
269
+
270
+ except Exception as e:
271
+ self.logger.error(f"Error cleaning up history: {e}")
272
+ return 0
273
+
274
+ def create_collection(self, name: str, item_timestamps: List[str] = None) -> bool:
275
+ """
276
+ Create a new collection.
277
+
278
+ Args:
279
+ name: Collection name
280
+ item_timestamps: List of history item timestamps to include
281
+
282
+ Returns:
283
+ True if successful
284
+ """
285
+ try:
286
+ if name in self.collections:
287
+ self.logger.warning(f"Collection '{name}' already exists")
288
+ return False
289
+
290
+ self.collections[name] = item_timestamps or []
291
+ self.save_history()
292
+ self.logger.info(f"Created collection: {name}")
293
+ return True
294
+
295
+ except Exception as e:
296
+ self.logger.error(f"Error creating collection: {e}")
297
+ return False
298
+
299
+ def add_to_collection(self, collection_name: str, timestamp: str) -> bool:
300
+ """
301
+ Add a history item to a collection.
302
+
303
+ Args:
304
+ collection_name: Name of the collection
305
+ timestamp: History item timestamp
306
+
307
+ Returns:
308
+ True if successful
309
+ """
310
+ try:
311
+ if collection_name not in self.collections:
312
+ self.collections[collection_name] = []
313
+
314
+ if timestamp not in self.collections[collection_name]:
315
+ # Verify the history item exists
316
+ if self.get_history_item(timestamp):
317
+ self.collections[collection_name].append(timestamp)
318
+ self.save_history()
319
+ self.logger.debug(f"Added item {timestamp} to collection {collection_name}")
320
+ return True
321
+ else:
322
+ self.logger.warning(f"History item {timestamp} not found")
323
+ return False
324
+ else:
325
+ self.logger.debug(f"Item {timestamp} already in collection {collection_name}")
326
+ return True
327
+
328
+ except Exception as e:
329
+ self.logger.error(f"Error adding to collection: {e}")
330
+ return False
331
+
332
+ def remove_from_collection(self, collection_name: str, timestamp: str) -> bool:
333
+ """
334
+ Remove a history item from a collection.
335
+
336
+ Args:
337
+ collection_name: Name of the collection
338
+ timestamp: History item timestamp
339
+
340
+ Returns:
341
+ True if successful
342
+ """
343
+ try:
344
+ if collection_name in self.collections:
345
+ if timestamp in self.collections[collection_name]:
346
+ self.collections[collection_name].remove(timestamp)
347
+ self.save_history()
348
+ self.logger.debug(f"Removed item {timestamp} from collection {collection_name}")
349
+ return True
350
+ return False
351
+
352
+ except Exception as e:
353
+ self.logger.error(f"Error removing from collection: {e}")
354
+ return False
355
+
356
+ def get_collection(self, name: str) -> List[RequestHistoryItem]:
357
+ """
358
+ Get items in a collection.
359
+
360
+ Args:
361
+ name: Collection name
362
+
363
+ Returns:
364
+ List of history items in the collection
365
+ """
366
+ if name not in self.collections:
367
+ return []
368
+
369
+ items = []
370
+ for timestamp in self.collections[name]:
371
+ item = self.get_history_item(timestamp)
372
+ if item:
373
+ items.append(item)
374
+
375
+ return sorted(items, key=lambda x: x.timestamp, reverse=True)
376
+
377
+ def get_collections(self) -> Dict[str, int]:
378
+ """
379
+ Get all collections with item counts.
380
+
381
+ Returns:
382
+ Dictionary of collection name -> item count
383
+ """
384
+ result = {}
385
+ for name, timestamps in self.collections.items():
386
+ # Count only existing items
387
+ count = sum(1 for ts in timestamps if self.get_history_item(ts))
388
+ result[name] = count
389
+ return result
390
+
391
+ def delete_collection(self, name: str) -> bool:
392
+ """
393
+ Delete a collection.
394
+
395
+ Args:
396
+ name: Collection name
397
+
398
+ Returns:
399
+ True if successful
400
+ """
401
+ try:
402
+ if name in self.collections:
403
+ del self.collections[name]
404
+ self.save_history()
405
+ self.logger.info(f"Deleted collection: {name}")
406
+ return True
407
+ return False
408
+
409
+ except Exception as e:
410
+ self.logger.error(f"Error deleting collection: {e}")
411
+ return False
412
+
413
+ def export_history(self, filepath: str, collection_name: str = None) -> bool:
414
+ """
415
+ Export history to a file.
416
+
417
+ Args:
418
+ filepath: Export file path
419
+ collection_name: Optional collection name to export
420
+
421
+ Returns:
422
+ True if successful
423
+ """
424
+ try:
425
+ if collection_name:
426
+ items = self.get_collection(collection_name)
427
+ export_data = {
428
+ "type": "collection",
429
+ "name": collection_name,
430
+ "items": [item.to_dict() for item in items]
431
+ }
432
+ else:
433
+ export_data = {
434
+ "type": "full_history",
435
+ "items": [item.to_dict() for item in self.history],
436
+ "collections": self.collections
437
+ }
438
+
439
+ export_data["export_date"] = datetime.now().isoformat()
440
+ export_data["version"] = "1.0"
441
+
442
+ with open(filepath, 'w', encoding='utf-8') as f:
443
+ json.dump(export_data, f, indent=2, ensure_ascii=False)
444
+
445
+ self.logger.info(f"History exported to {filepath}")
446
+ return True
447
+
448
+ except Exception as e:
449
+ self.logger.error(f"Error exporting history: {e}")
450
+ return False
451
+
452
+ def import_history(self, filepath: str, merge: bool = True) -> bool:
453
+ """
454
+ Import history from a file.
455
+
456
+ Args:
457
+ filepath: Import file path
458
+ merge: Whether to merge with existing history
459
+
460
+ Returns:
461
+ True if successful
462
+ """
463
+ try:
464
+ with open(filepath, 'r', encoding='utf-8') as f:
465
+ import_data = json.load(f)
466
+
467
+ if not merge:
468
+ self.history.clear()
469
+ self.collections.clear()
470
+
471
+ # Import items
472
+ items_data = import_data.get("items", [])
473
+ for item_data in items_data:
474
+ try:
475
+ item = RequestHistoryItem.from_dict(item_data)
476
+ # Check if item already exists (by timestamp)
477
+ if not any(h.timestamp == item.timestamp for h in self.history):
478
+ self.history.append(item)
479
+ except Exception as e:
480
+ self.logger.warning(f"Error importing history item: {e}")
481
+
482
+ # Import collections if present
483
+ if "collections" in import_data:
484
+ for name, timestamps in import_data["collections"].items():
485
+ if name not in self.collections:
486
+ self.collections[name] = []
487
+ # Add timestamps that don't already exist
488
+ for ts in timestamps:
489
+ if ts not in self.collections[name]:
490
+ self.collections[name].append(ts)
491
+
492
+ # Maintain max items limit
493
+ if len(self.history) > self.max_items:
494
+ # Sort by timestamp and keep newest
495
+ self.history.sort(key=lambda x: x.timestamp, reverse=True)
496
+ self.history = self.history[:self.max_items]
497
+
498
+ self.save_history()
499
+ self.logger.info(f"History imported from {filepath}")
500
+ return True
501
+
502
+ except Exception as e:
503
+ self.logger.error(f"Error importing history: {e}")
504
+ return False
505
+
506
+ def load_history(self) -> bool:
507
+ """
508
+ Load history from centralized settings.json file.
509
+
510
+ Returns:
511
+ True if successful
512
+ """
513
+ try:
514
+ if os.path.exists(self.history_file):
515
+ with open(self.history_file, 'r', encoding='utf-8') as f:
516
+ all_settings = json.load(f)
517
+
518
+ # Get tool settings from tool_settings section
519
+ tool_settings = all_settings.get("tool_settings", {})
520
+ curl_settings = tool_settings.get(self.tool_key, {})
521
+
522
+ # Load history items
523
+ self.history = []
524
+ for item_data in curl_settings.get("history", []):
525
+ try:
526
+ item = RequestHistoryItem.from_dict(item_data)
527
+ self.history.append(item)
528
+ except Exception as e:
529
+ self.logger.warning(f"Error loading history item: {e}")
530
+
531
+ # Load collections
532
+ self.collections = curl_settings.get("collections", {})
533
+
534
+ self.logger.info(f"Loaded {len(self.history)} history items from {self.history_file}")
535
+ return True
536
+ else:
537
+ self.logger.info("No history file found, starting with empty history")
538
+ return True
539
+
540
+ except Exception as e:
541
+ self.logger.error(f"Error loading history: {e}")
542
+ return False
543
+
544
+ def save_history(self) -> bool:
545
+ """
546
+ Save history to centralized settings.json file.
547
+
548
+ Returns:
549
+ True if successful
550
+ """
551
+ try:
552
+ # Load existing settings file
553
+ all_settings = {}
554
+ if os.path.exists(self.history_file):
555
+ with open(self.history_file, 'r', encoding='utf-8') as f:
556
+ all_settings = json.load(f)
557
+
558
+ # Ensure tool_settings section exists
559
+ if "tool_settings" not in all_settings:
560
+ all_settings["tool_settings"] = {}
561
+
562
+ # Ensure cURL Tool section exists
563
+ if self.tool_key not in all_settings["tool_settings"]:
564
+ all_settings["tool_settings"][self.tool_key] = {}
565
+
566
+ # Update history and collections in cURL Tool settings
567
+ all_settings["tool_settings"][self.tool_key]["history"] = [item.to_dict() for item in self.history]
568
+ all_settings["tool_settings"][self.tool_key]["collections"] = self.collections
569
+ all_settings["tool_settings"][self.tool_key]["history_last_updated"] = datetime.now().isoformat()
570
+ all_settings["tool_settings"][self.tool_key]["history_version"] = "1.0"
571
+
572
+ # Write settings to file
573
+ with open(self.history_file, 'w', encoding='utf-8') as f:
574
+ json.dump(all_settings, f, indent=4, ensure_ascii=False)
575
+
576
+ self.logger.debug(f"History saved to {self.history_file}")
577
+ return True
578
+
579
+ except Exception as e:
580
+ self.logger.error(f"Error saving history: {e}")
581
+ return False
582
+
583
+ def _remove_from_collections(self, timestamp: str):
584
+ """Remove a timestamp from all collections."""
585
+ for collection_name in list(self.collections.keys()):
586
+ if timestamp in self.collections[collection_name]:
587
+ self.collections[collection_name].remove(timestamp)
588
+
589
+ def _cleanup_collections(self):
590
+ """Clean up collections by removing references to non-existent items."""
591
+ for collection_name in list(self.collections.keys()):
592
+ # Filter out timestamps that don't exist in history
593
+ valid_timestamps = [
594
+ ts for ts in self.collections[collection_name]
595
+ if self.get_history_item(ts) is not None
596
+ ]
597
+ self.collections[collection_name] = valid_timestamps
598
+
599
+ # Remove empty collections
600
+ if not self.collections[collection_name]:
601
601
  del self.collections[collection_name]