mcli-framework 7.0.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.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
mcli/app/logs_cmd.py ADDED
@@ -0,0 +1,419 @@
1
+ """
2
+ Log streaming and management commands
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import time
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import click
14
+ from rich.console import Console
15
+ from rich.live import Live
16
+ from rich.panel import Panel
17
+ from rich.text import Text
18
+
19
+ console = Console()
20
+
21
+
22
+ @click.group(name="logs")
23
+ def logs_group():
24
+ """Stream and manage MCLI log files"""
25
+ pass
26
+
27
+
28
+ @logs_group.command(name="stream")
29
+ @click.option(
30
+ "--type",
31
+ "-t",
32
+ type=click.Choice(["main", "trace", "system", "all"]),
33
+ default="main",
34
+ help="Type of logs to stream (default: main)",
35
+ )
36
+ @click.option(
37
+ "--lines", "-n", type=int, default=50, help="Number of initial lines to show (default: 50)"
38
+ )
39
+ @click.option(
40
+ "--follow",
41
+ "-f",
42
+ is_flag=True,
43
+ default=True,
44
+ help="Follow log output in real-time (default: enabled)",
45
+ )
46
+ def stream_logs(type: str, lines: int, follow: bool):
47
+ """Stream MCLI log files in real-time
48
+
49
+ Shows log output with syntax highlighting and real-time updates.
50
+ Similar to 'tail -f' but with enhanced formatting for MCLI logs.
51
+ """
52
+ logs_dir = Path("logs")
53
+
54
+ if not logs_dir.exists():
55
+ console.print(
56
+ "❌ No logs directory found. Run some MCLI commands to generate logs.", style="red"
57
+ )
58
+ return
59
+
60
+ # Get today's log files
61
+ today = datetime.now().strftime("%Y%m%d")
62
+
63
+ log_files = {
64
+ "main": logs_dir / f"mcli_{today}.log",
65
+ "trace": logs_dir / f"mcli_trace_{today}.log",
66
+ "system": logs_dir / f"mcli_system_{today}.log",
67
+ }
68
+
69
+ # Filter to existing files
70
+ existing_files = {k: v for k, v in log_files.items() if v.exists()}
71
+
72
+ if not existing_files:
73
+ console.print(f"❌ No log files found for today ({today})", style="red")
74
+ _list_available_logs(logs_dir)
75
+ return
76
+
77
+ # Determine which files to stream
78
+ if type == "all":
79
+ files_to_stream = list(existing_files.values())
80
+ else:
81
+ if type not in existing_files:
82
+ console.print(f"❌ {type} log file not found for today", style="red")
83
+ console.print(f"Available logs: {', '.join(existing_files.keys())}", style="yellow")
84
+ return
85
+ files_to_stream = [existing_files[type]]
86
+
87
+ # Start streaming
88
+ try:
89
+ if len(files_to_stream) == 1:
90
+ _stream_single_file(files_to_stream[0], lines, follow)
91
+ else:
92
+ _stream_multiple_files(files_to_stream, lines, follow)
93
+ except KeyboardInterrupt:
94
+ console.print("\n👋 Log streaming stopped", style="cyan")
95
+
96
+
97
+ @logs_group.command(name="list")
98
+ @click.option("--date", "-d", help="Show logs for specific date (YYYYMMDD format)")
99
+ def list_logs(date: Optional[str]):
100
+ """List available log files"""
101
+ logs_dir = Path("logs")
102
+
103
+ if not logs_dir.exists():
104
+ console.print("❌ No logs directory found", style="red")
105
+ return
106
+
107
+ _list_available_logs(logs_dir, date)
108
+
109
+
110
+ @logs_group.command(name="tail")
111
+ @click.argument("log_type", type=click.Choice(["main", "trace", "system"]))
112
+ @click.option("--lines", "-n", type=int, default=20, help="Number of lines to show (default: 20)")
113
+ @click.option("--date", "-d", help="Date for log file (YYYYMMDD format, default: today)")
114
+ def tail_logs(log_type: str, lines: int, date: Optional[str]):
115
+ """Show the last N lines of a specific log file"""
116
+ logs_dir = Path("logs")
117
+
118
+ if not logs_dir.exists():
119
+ console.print("❌ No logs directory found", style="red")
120
+ return
121
+
122
+ # Use provided date or default to today
123
+ log_date = date or datetime.now().strftime("%Y%m%d")
124
+
125
+ # Map log types to file patterns
126
+ log_files = {
127
+ "main": f"mcli_{log_date}.log",
128
+ "trace": f"mcli_trace_{log_date}.log",
129
+ "system": f"mcli_system_{log_date}.log",
130
+ }
131
+
132
+ log_file = logs_dir / log_files[log_type]
133
+
134
+ if not log_file.exists():
135
+ console.print(f"❌ Log file not found: {log_file}", style="red")
136
+ _list_available_logs(logs_dir, log_date)
137
+ return
138
+
139
+ try:
140
+ # Read last N lines
141
+ with open(log_file, "r") as f:
142
+ all_lines = f.readlines()
143
+ tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
144
+
145
+ # Display with formatting
146
+ console.print(f"\n📋 **Last {len(tail_lines)} lines from {log_file.name}**\n", style="cyan")
147
+
148
+ for line in tail_lines:
149
+ formatted_line = _format_log_line(line.rstrip())
150
+ console.print(formatted_line)
151
+
152
+ except Exception as e:
153
+ console.print(f"❌ Error reading log file: {e}", style="red")
154
+
155
+
156
+ @logs_group.command(name="grep")
157
+ @click.argument("pattern")
158
+ @click.option(
159
+ "--type",
160
+ "-t",
161
+ type=click.Choice(["main", "trace", "system", "all"]),
162
+ default="all",
163
+ help="Type of logs to search (default: all)",
164
+ )
165
+ @click.option("--date", "-d", help="Date for log files (YYYYMMDD format, default: today)")
166
+ @click.option(
167
+ "--context", "-C", type=int, default=3, help="Lines of context around matches (default: 3)"
168
+ )
169
+ def grep_logs(pattern: str, type: str, date: Optional[str], context: int):
170
+ """Search for patterns in log files"""
171
+ logs_dir = Path("logs")
172
+
173
+ if not logs_dir.exists():
174
+ console.print("❌ No logs directory found", style="red")
175
+ return
176
+
177
+ # Use provided date or default to today
178
+ log_date = date or datetime.now().strftime("%Y%m%d")
179
+
180
+ # Get log files to search
181
+ log_files = {
182
+ "main": logs_dir / f"mcli_{log_date}.log",
183
+ "trace": logs_dir / f"mcli_trace_{log_date}.log",
184
+ "system": logs_dir / f"mcli_system_{log_date}.log",
185
+ }
186
+
187
+ files_to_search = []
188
+ if type == "all":
189
+ files_to_search = [f for f in log_files.values() if f.exists()]
190
+ else:
191
+ if log_files[type].exists():
192
+ files_to_search = [log_files[type]]
193
+
194
+ if not files_to_search:
195
+ console.print(f"❌ No log files found for {log_date}", style="red")
196
+ return
197
+
198
+ # Search each file
199
+ total_matches = 0
200
+ for log_file in files_to_search:
201
+ matches = _search_log_file(log_file, pattern, context)
202
+ if matches:
203
+ console.print(f"\n📁 **{log_file.name}** ({len(matches)} matches)", style="cyan")
204
+ for match in matches:
205
+ console.print(match)
206
+ total_matches += len(matches)
207
+
208
+ if total_matches == 0:
209
+ console.print(f"❌ No matches found for pattern: {pattern}", style="yellow")
210
+ else:
211
+ console.print(f"\n✅ Found {total_matches} total matches", style="green")
212
+
213
+
214
+ @logs_group.command(name="clear")
215
+ @click.option("--older-than", type=int, help="Clear logs older than N days")
216
+ @click.option("--confirm", "-y", is_flag=True, help="Skip confirmation prompt")
217
+ def clear_logs(older_than: Optional[int], confirm: bool):
218
+ """Clear old log files"""
219
+ logs_dir = Path("logs")
220
+
221
+ if not logs_dir.exists():
222
+ console.print("❌ No logs directory found", style="red")
223
+ return
224
+
225
+ # Find log files
226
+ log_files = list(logs_dir.glob("mcli*.log"))
227
+
228
+ if not log_files:
229
+ console.print("ℹ️ No log files to clear", style="blue")
230
+ return
231
+
232
+ # Filter by age if specified
233
+ if older_than:
234
+ import time
235
+
236
+ cutoff_time = time.time() - (older_than * 24 * 60 * 60) # Convert days to seconds
237
+ log_files = [f for f in log_files if f.stat().st_mtime < cutoff_time]
238
+
239
+ if not log_files:
240
+ console.print(f"ℹ️ No log files older than {older_than} days", style="blue")
241
+ return
242
+
243
+ # Show what will be deleted
244
+ console.print(f"📋 **Log files to clear:**", style="yellow")
245
+ total_size = 0
246
+ for log_file in log_files:
247
+ size = log_file.stat().st_size
248
+ total_size += size
249
+ console.print(f" • {log_file.name} ({_format_file_size(size)})")
250
+
251
+ console.print(f"\n📊 **Total size:** {_format_file_size(total_size)}", style="cyan")
252
+
253
+ # Confirm deletion
254
+ if not confirm:
255
+ if not click.confirm(f"\nDelete {len(log_files)} log files?"):
256
+ console.print("❌ Operation cancelled", style="yellow")
257
+ return
258
+
259
+ # Delete files
260
+ deleted_count = 0
261
+ for log_file in log_files:
262
+ try:
263
+ log_file.unlink()
264
+ deleted_count += 1
265
+ except Exception as e:
266
+ console.print(f"❌ Failed to delete {log_file.name}: {e}", style="red")
267
+
268
+ console.print(
269
+ f"✅ Deleted {deleted_count} log files ({_format_file_size(total_size)} freed)",
270
+ style="green",
271
+ )
272
+
273
+
274
+ def _stream_single_file(log_file: Path, lines: int, follow: bool):
275
+ """Stream a single log file"""
276
+ console.print(f"📡 **Streaming {log_file.name}**", style="cyan")
277
+ console.print("Press Ctrl+C to stop\n")
278
+
279
+ if follow:
280
+ # Use tail -f for real-time following
281
+ cmd = ["tail", f"-{lines}", "-f", str(log_file)]
282
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
283
+
284
+ try:
285
+ for line in iter(process.stdout.readline, ""):
286
+ if line:
287
+ formatted_line = _format_log_line(line.rstrip())
288
+ console.print(formatted_line)
289
+ except KeyboardInterrupt:
290
+ process.terminate()
291
+ else:
292
+ # Just show last N lines
293
+ with open(log_file, "r") as f:
294
+ all_lines = f.readlines()
295
+ tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
296
+
297
+ for line in tail_lines:
298
+ formatted_line = _format_log_line(line.rstrip())
299
+ console.print(formatted_line)
300
+
301
+
302
+ def _stream_multiple_files(log_files: list, lines: int, follow: bool):
303
+ """Stream multiple log files simultaneously"""
304
+ console.print(f"📡 **Streaming {len(log_files)} log files**", style="cyan")
305
+ for log_file in log_files:
306
+ console.print(f" • {log_file.name}")
307
+ console.print("Press Ctrl+C to stop\n")
308
+
309
+ # Use multitail or custom implementation
310
+ # For simplicity, we'll cycle through files
311
+ try:
312
+ while True:
313
+ for log_file in log_files:
314
+ if log_file.exists():
315
+ # Show recent lines from each file
316
+ with open(log_file, "r") as f:
317
+ all_lines = f.readlines()
318
+ recent_lines = all_lines[-5:] if len(all_lines) > 5 else all_lines
319
+
320
+ if recent_lines:
321
+ console.print(f"\n--- {log_file.name} ---", style="blue")
322
+ for line in recent_lines:
323
+ formatted_line = _format_log_line(line.rstrip())
324
+ console.print(formatted_line)
325
+
326
+ if follow:
327
+ time.sleep(2) # Update every 2 seconds
328
+ else:
329
+ break
330
+
331
+ except KeyboardInterrupt:
332
+ pass
333
+
334
+
335
+ def _format_log_line(line: str) -> Text:
336
+ """Format a log line with syntax highlighting"""
337
+ text = Text()
338
+
339
+ # Color-code by log level
340
+ if "ERROR" in line:
341
+ text.append(line, style="red")
342
+ elif "WARNING" in line or "WARN" in line:
343
+ text.append(line, style="yellow")
344
+ elif "INFO" in line:
345
+ text.append(line, style="green")
346
+ elif "DEBUG" in line:
347
+ text.append(line, style="dim blue")
348
+ else:
349
+ text.append(line, style="white")
350
+
351
+ return text
352
+
353
+
354
+ def _list_available_logs(logs_dir: Path, date_filter: Optional[str] = None):
355
+ """List available log files"""
356
+ log_files = sorted(logs_dir.glob("mcli*.log"))
357
+
358
+ if not log_files:
359
+ console.print("ℹ️ No log files found", style="blue")
360
+ return
361
+
362
+ # Filter by date if specified
363
+ if date_filter:
364
+ log_files = [f for f in log_files if date_filter in f.name]
365
+ if not log_files:
366
+ console.print(f"ℹ️ No log files found for date: {date_filter}", style="blue")
367
+ return
368
+
369
+ console.print("📋 **Available log files:**", style="cyan")
370
+
371
+ for log_file in log_files:
372
+ size = log_file.stat().st_size
373
+ mtime = datetime.fromtimestamp(log_file.stat().st_mtime)
374
+ console.print(
375
+ f" • {log_file.name} ({_format_file_size(size)}, {mtime.strftime('%Y-%m-%d %H:%M')})"
376
+ )
377
+
378
+
379
+ def _search_log_file(log_file: Path, pattern: str, context: int) -> list:
380
+ """Search for pattern in log file with context"""
381
+ matches = []
382
+
383
+ try:
384
+ with open(log_file, "r") as f:
385
+ lines = f.readlines()
386
+
387
+ for i, line in enumerate(lines):
388
+ if pattern.lower() in line.lower():
389
+ # Get context lines
390
+ start = max(0, i - context)
391
+ end = min(len(lines), i + context + 1)
392
+
393
+ context_lines = []
394
+ for j in range(start, end):
395
+ prefix = ">>> " if j == i else " "
396
+ style = "bright_yellow" if j == i else "dim"
397
+ context_lines.append(f"{prefix}{lines[j].rstrip()}")
398
+
399
+ matches.append("\n".join(context_lines) + "\n")
400
+
401
+ except Exception as e:
402
+ console.print(f"❌ Error searching {log_file.name}: {e}", style="red")
403
+
404
+ return matches
405
+
406
+
407
+ def _format_file_size(size_bytes: int) -> str:
408
+ """Format file size in human readable format"""
409
+ for unit in ["B", "KB", "MB", "GB"]:
410
+ if size_bytes < 1024.0:
411
+ return f"{size_bytes:.1f} {unit}"
412
+ size_bytes /= 1024.0
413
+ return f"{size_bytes:.1f} TB"
414
+
415
+
416
+ # Register with main CLI
417
+ def register_logs_commands(cli):
418
+ """Register logs commands with the main CLI"""
419
+ cli.add_command(logs_group)