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/self/self_cmd.py ADDED
@@ -0,0 +1,1246 @@
1
+ """
2
+ Self-management commands for mcli.
3
+ Provides utilities for maintaining and extending the CLI itself.
4
+ """
5
+
6
+ import hashlib
7
+ import importlib
8
+ import inspect
9
+ import json
10
+ import os
11
+ import re
12
+ import time
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ import click
18
+ import tomli
19
+ from rich.console import Console
20
+ from rich.prompt import Prompt
21
+ from rich.table import Table
22
+
23
+ try:
24
+ import warnings
25
+
26
+ # Suppress the warning about python-Levenshtein
27
+ warnings.filterwarnings("ignore", message="Using slow pure-python SequenceMatcher")
28
+ from fuzzywuzzy import process
29
+ except ImportError:
30
+ process = None
31
+
32
+ from mcli.lib.logger.logger import get_logger
33
+
34
+ logger = get_logger()
35
+
36
+
37
+ # Create a Click command group instead of Typer
38
+ @click.group(name="self", help="Manage and extend the mcli application")
39
+ def self_app():
40
+ """
41
+ Self-management commands for mcli.
42
+ """
43
+ pass
44
+
45
+
46
+ console = Console()
47
+
48
+ LOCKFILE_PATH = Path.home() / ".local" / "mcli" / "command_lock.json"
49
+
50
+ # Utility functions for command state lockfile
51
+
52
+
53
+ def get_current_command_state():
54
+ """Collect all command metadata (names, groups, etc.)"""
55
+ # This should use your actual command collection logic
56
+ # For now, use the collect_commands() function
57
+ return collect_commands()
58
+
59
+
60
+ def hash_command_state(commands):
61
+ """Hash the command state for fast comparison."""
62
+ # Sort for deterministic hash
63
+ commands_sorted = sorted(commands, key=lambda c: (c.get("group") or "", c["name"]))
64
+ state_json = json.dumps(commands_sorted, sort_keys=True)
65
+ return hashlib.sha256(state_json.encode("utf-8")).hexdigest()
66
+
67
+
68
+ def load_lockfile():
69
+ if LOCKFILE_PATH.exists():
70
+ with open(LOCKFILE_PATH, "r") as f:
71
+ return json.load(f)
72
+ return []
73
+
74
+
75
+ def save_lockfile(states):
76
+ with open(LOCKFILE_PATH, "w") as f:
77
+ json.dump(states, f, indent=2, default=str)
78
+
79
+
80
+ def append_lockfile(new_state):
81
+ states = load_lockfile()
82
+ states.append(new_state)
83
+ save_lockfile(states)
84
+
85
+
86
+ def find_state_by_hash(hash_value):
87
+ states = load_lockfile()
88
+ for state in states:
89
+ if state["hash"] == hash_value:
90
+ return state
91
+ return None
92
+
93
+
94
+ def restore_command_state(hash_value):
95
+ state = find_state_by_hash(hash_value)
96
+ if not state:
97
+ return False
98
+ # Here you would implement logic to restore the command registry to this state
99
+ # For now, just print the commands
100
+ print(json.dumps(state["commands"], indent=2))
101
+ return True
102
+
103
+
104
+ # Create a Click group for all command management
105
+ @self_app.group("commands")
106
+ def commands_group():
107
+ """Manage CLI commands and command state."""
108
+ pass
109
+
110
+
111
+ # Move the command-state group under commands_group
112
+ @commands_group.group("state")
113
+ def command_state():
114
+ """Manage command state lockfile and history."""
115
+ pass
116
+
117
+
118
+ @command_state.command("list")
119
+ def list_states():
120
+ """List all saved command states (hash, timestamp, #commands)."""
121
+ states = load_lockfile()
122
+ if not states:
123
+ click.echo("No command states found.")
124
+ return
125
+ table = Table(title="Command States")
126
+ table.add_column("Hash", style="cyan")
127
+ table.add_column("Timestamp", style="green")
128
+ table.add_column("# Commands", style="yellow")
129
+ for state in states:
130
+ table.add_row(state["hash"][:8], state["timestamp"], str(len(state["commands"])))
131
+ console.print(table)
132
+
133
+
134
+ @command_state.command("restore")
135
+ @click.argument("hash_value")
136
+ def restore_state(hash_value):
137
+ """Restore to a previous command state by hash."""
138
+ if restore_command_state(hash_value):
139
+ click.echo(f"Restored to state {hash_value[:8]}")
140
+ else:
141
+ click.echo(f"State {hash_value[:8]} not found.", err=True)
142
+
143
+
144
+ @command_state.command("write")
145
+ @click.argument("json_file", required=False, type=click.Path(exists=False))
146
+ def write_state(json_file):
147
+ """Write a new command state to the lockfile from a JSON file or the current app state."""
148
+ import traceback
149
+
150
+ print("[DEBUG] write_state called")
151
+ print(f"[DEBUG] LOCKFILE_PATH: {LOCKFILE_PATH}")
152
+ try:
153
+ if json_file:
154
+ print(f"[DEBUG] Loading command state from file: {json_file}")
155
+ with open(json_file, "r") as f:
156
+ commands = json.load(f)
157
+ click.echo(f"Loaded command state from {json_file}.")
158
+ else:
159
+ print("[DEBUG] Snapshotting current command state.")
160
+ commands = get_current_command_state()
161
+ state_hash = hash_command_state(commands)
162
+ new_state = {
163
+ "hash": state_hash,
164
+ "timestamp": datetime.utcnow().isoformat() + "Z",
165
+ "commands": commands,
166
+ }
167
+ append_lockfile(new_state)
168
+ print(f"[DEBUG] Wrote new command state {state_hash[:8]} to lockfile at {LOCKFILE_PATH}")
169
+ click.echo(f"Wrote new command state {state_hash[:8]} to lockfile.")
170
+ except Exception as e:
171
+ print(f"[ERROR] Exception in write_state: {e}")
172
+ print(traceback.format_exc())
173
+ click.echo(f"[ERROR] Failed to write command state: {e}", err=True)
174
+
175
+
176
+ # On CLI startup, check and update lockfile if needed
177
+
178
+
179
+ def check_and_update_command_lockfile():
180
+ current_commands = get_current_command_state()
181
+ current_hash = hash_command_state(current_commands)
182
+ states = load_lockfile()
183
+ if states and states[-1]["hash"] == current_hash:
184
+ # No change
185
+ return
186
+ # New state, append
187
+ new_state = {
188
+ "hash": current_hash,
189
+ "timestamp": datetime.utcnow().isoformat() + "Z",
190
+ "commands": current_commands,
191
+ }
192
+ append_lockfile(new_state)
193
+ logger.info(f"Appended new command state {current_hash[:8]} to lockfile.")
194
+
195
+
196
+ # Call this at the top of your CLI entrypoint (main.py or similar)
197
+ # check_and_update_command_lockfile()
198
+
199
+
200
+ def get_command_template(name: str, group: Optional[str] = None) -> str:
201
+ """Generate template code for a new command."""
202
+
203
+ if group:
204
+ # Template for a command in a group using Click
205
+ template = f'''"""
206
+ {name} command for mcli.{group}.
207
+ """
208
+ import click
209
+ from typing import Optional, List
210
+ from pathlib import Path
211
+ from mcli.lib.logger.logger import get_logger
212
+
213
+ logger = get_logger()
214
+
215
+ # Create a Click command group
216
+ @click.group(name="{name}")
217
+ def {name}_group():
218
+ """Description for {name} command group."""
219
+ pass
220
+
221
+ @{name}_group.command("hello")
222
+ @click.argument("name", default="World")
223
+ def hello(name: str):
224
+ """Example subcommand."""
225
+ logger.info(f"Hello, {{name}}! This is the {name} command.")
226
+ click.echo(f"Hello, {{name}}! This is the {name} command.")
227
+ '''
228
+ else:
229
+ # Template for a command directly under self using Click
230
+ template = f'''"""
231
+ {name} command for mcli.self.
232
+ """
233
+ import click
234
+ from typing import Optional, List
235
+ from pathlib import Path
236
+ from mcli.lib.logger.logger import get_logger
237
+
238
+ logger = get_logger()
239
+
240
+ def {name}_command(name: str = "World"):
241
+ """
242
+ {name.capitalize()} command.
243
+ """
244
+ logger.info(f"Hello, {{name}}! This is the {name} command.")
245
+ click.echo(f"Hello, {{name}}! This is the {name} command.")
246
+ '''
247
+
248
+ return template
249
+
250
+
251
+ @self_app.command("search")
252
+ @click.argument("query", required=False)
253
+ @click.option("--full", "-f", is_flag=True, help="Show full command paths and descriptions")
254
+ def search(query, full):
255
+ """
256
+ Search for available commands using fuzzy matching.
257
+
258
+ Similar to telescope in neovim, this allows quick fuzzy searching
259
+ through all available commands in mcli.
260
+
261
+ If no query is provided, lists all commands.
262
+ """
263
+ # Collect all commands from the application
264
+ commands = collect_commands()
265
+
266
+ # Display the commands in a table
267
+ table = Table(title="mcli Commands")
268
+ table.add_column("Command", style="green")
269
+ table.add_column("Group", style="blue")
270
+ if full:
271
+ table.add_column("Path", style="dim")
272
+ table.add_column("Description", style="yellow")
273
+
274
+ if query:
275
+ filtered_commands = []
276
+
277
+ # Try to use fuzzywuzzy for better matching if available
278
+ if process:
279
+ # Extract command names for matching
280
+ command_names = [
281
+ f"{cmd['group']}.{cmd['name']}" if cmd["group"] else cmd["name"] for cmd in commands
282
+ ]
283
+ matches = process.extract(query, command_names, limit=10)
284
+
285
+ # Filter to matched commands
286
+ match_indices = [command_names.index(match[0]) for match in matches if match[1] > 50]
287
+ filtered_commands = [commands[i] for i in match_indices]
288
+ else:
289
+ # Fallback to simple substring matching
290
+ filtered_commands = [
291
+ cmd
292
+ for cmd in commands
293
+ if query.lower() in cmd["name"].lower()
294
+ or (cmd["group"] and query.lower() in cmd["group"].lower())
295
+ ]
296
+
297
+ commands = filtered_commands
298
+
299
+ # Sort commands by group then name
300
+ commands.sort(key=lambda c: (c["group"] if c["group"] else "", c["name"]))
301
+
302
+ # Add rows to the table
303
+ for cmd in commands:
304
+ if full:
305
+ table.add_row(
306
+ cmd["name"],
307
+ cmd["group"] if cmd["group"] else "-",
308
+ cmd["path"],
309
+ cmd["help"] if cmd["help"] else "",
310
+ )
311
+ else:
312
+ table.add_row(cmd["name"], cmd["group"] if cmd["group"] else "-")
313
+
314
+ console.print(table)
315
+
316
+ if not commands:
317
+ logger.info("No commands found matching the search query")
318
+ click.echo("No commands found matching the search query")
319
+
320
+ return 0
321
+
322
+
323
+ def collect_commands() -> List[Dict[str, Any]]:
324
+ """Collect all commands from the mcli application."""
325
+ commands = []
326
+
327
+ # Look for command modules in the mcli package
328
+ mcli_path = Path(__file__).parent.parent
329
+
330
+ # This finds command groups as directories under mcli
331
+ for item in mcli_path.iterdir():
332
+ if item.is_dir() and not item.name.startswith("__") and not item.name.startswith("."):
333
+ group_name = item.name
334
+
335
+ # Recursively find all Python files that might define commands
336
+ for py_file in item.glob("**/*.py"):
337
+ if py_file.name.startswith("__"):
338
+ continue
339
+
340
+ # Convert file path to module path
341
+ relative_path = py_file.relative_to(mcli_path.parent)
342
+ module_name = ".".join(relative_path.with_suffix("").parts)
343
+
344
+ try:
345
+ # Try to import the module
346
+ module = importlib.import_module(module_name)
347
+
348
+ # Extract command and group objects
349
+ for name, obj in inspect.getmembers(module):
350
+ # Handle Click commands and groups
351
+ if isinstance(obj, click.Command):
352
+ if isinstance(obj, click.Group):
353
+ # Found a Click group
354
+ app_info = {
355
+ "name": obj.name,
356
+ "group": group_name,
357
+ "path": module_name,
358
+ "help": obj.help,
359
+ }
360
+ commands.append(app_info)
361
+
362
+ # Add subcommands if any
363
+ for cmd_name, cmd in obj.commands.items():
364
+ commands.append(
365
+ {
366
+ "name": cmd_name,
367
+ "group": f"{group_name}.{app_info['name']}",
368
+ "path": f"{module_name}.{cmd_name}",
369
+ "help": cmd.help,
370
+ }
371
+ )
372
+ else:
373
+ # Found a standalone Click command
374
+ commands.append(
375
+ {
376
+ "name": obj.name,
377
+ "group": group_name,
378
+ "path": f"{module_name}.{obj.name}",
379
+ "help": obj.help,
380
+ }
381
+ )
382
+ except (ImportError, AttributeError) as e:
383
+ logger.debug(f"Skipping {module_name}: {e}")
384
+
385
+ return commands
386
+
387
+
388
+ @self_app.command("add-command")
389
+ @click.argument("command_name", required=True)
390
+ @click.option("--group", "-g", help="Optional command group to create under")
391
+ def add_command(command_name, group):
392
+ """
393
+ Generate a new command template that can be used by mcli.
394
+
395
+ Example:
396
+ mcli self add my_command
397
+ mcli self add feature_command --group features
398
+ """
399
+ command_name = command_name.lower().replace("-", "_")
400
+
401
+ # Validate command name
402
+ if not re.match(r"^[a-z][a-z0-9_]*$", command_name):
403
+ logger.error(
404
+ f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter)."
405
+ )
406
+ click.echo(
407
+ f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter).",
408
+ err=True,
409
+ )
410
+ return 1
411
+
412
+ mcli_path = Path(__file__).parent.parent
413
+
414
+ if group:
415
+ # Creating under a specific group
416
+ command_group = group.lower().replace("-", "_")
417
+
418
+ # Validate group name
419
+ if not re.match(r"^[a-z][a-z0-9_]*$", command_group):
420
+ logger.error(
421
+ f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter)."
422
+ )
423
+ click.echo(
424
+ f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter).",
425
+ err=True,
426
+ )
427
+ return 1
428
+
429
+ # Check if group exists, create if needed
430
+ group_path = mcli_path / command_group
431
+ if not group_path.exists():
432
+ # Create group directory and __init__.py
433
+ group_path.mkdir(parents=True, exist_ok=True)
434
+ with open(group_path / "__init__.py", "w") as f:
435
+ f.write(f'"""\n{command_group.capitalize()} commands for mcli.\n"""')
436
+ logger.info(f"Created new command group directory: {command_group}")
437
+ click.echo(f"Created new command group directory: {command_group}")
438
+
439
+ # Create command file
440
+ command_file_path = group_path / f"{command_name}.py"
441
+ if command_file_path.exists():
442
+ logger.warning(f"Command file already exists: {command_file_path}")
443
+ should_override = Prompt.ask(
444
+ "File already exists. Override?", choices=["y", "n"], default="n"
445
+ )
446
+ if should_override.lower() != "y":
447
+ logger.info("Command creation aborted.")
448
+ click.echo("Command creation aborted.")
449
+ return 1
450
+
451
+ # Generate command file
452
+ with open(command_file_path, "w") as f:
453
+ f.write(get_command_template(command_name, command_group))
454
+
455
+ logger.info(f"Created new command: {command_name} in group: {command_group}")
456
+ click.echo(f"Created new command: {command_name} in group: {command_group}")
457
+ click.echo(f"File created: {command_file_path}")
458
+ click.echo(
459
+ f"To use this command, add 'from mcli.{command_group}.{command_name} import {command_name}_group' to your main imports"
460
+ )
461
+ click.echo(
462
+ f"Then add '{command_name}_group to your main CLI group using app.add_command({command_name}_group)'"
463
+ )
464
+
465
+ else:
466
+ # Creating directly under self
467
+ command_file_path = mcli_path / "self" / f"{command_name}.py"
468
+
469
+ if command_file_path.exists():
470
+ logger.warning(f"Command file already exists: {command_file_path}")
471
+ should_override = Prompt.ask(
472
+ "File already exists. Override?", choices=["y", "n"], default="n"
473
+ )
474
+ if should_override.lower() != "y":
475
+ logger.info("Command creation aborted.")
476
+ click.echo("Command creation aborted.")
477
+ return 1
478
+
479
+ # Generate command file
480
+ with open(command_file_path, "w") as f:
481
+ f.write(get_command_template(command_name))
482
+
483
+ # Update self_cmd.py to import and register the new command
484
+ with open(Path(__file__), "r") as f:
485
+ content = f.read()
486
+
487
+ # Add import statement if not exists
488
+ import_statement = f"from mcli.self.{command_name} import {command_name}_command"
489
+ if import_statement not in content:
490
+ import_section_end = content.find("logger = get_logger()")
491
+ if import_section_end != -1:
492
+ updated_content = (
493
+ content[:import_section_end]
494
+ + import_statement
495
+ + "\n"
496
+ + content[import_section_end:]
497
+ )
498
+
499
+ # Add command registration (Click syntax)
500
+ registration = f"@self_app.command('{command_name}')\ndef {command_name}(name=\"World\"):\n return {command_name}_command(name)\n"
501
+ registration_point = updated_content.rfind("def ")
502
+ if registration_point != -1:
503
+ # Find the end of the last function
504
+ last_func_end = updated_content.find("\n\n", registration_point)
505
+ if last_func_end != -1:
506
+ updated_content = (
507
+ updated_content[: last_func_end + 2]
508
+ + registration
509
+ + updated_content[last_func_end + 2 :]
510
+ )
511
+ else:
512
+ updated_content += "\n\n" + registration
513
+
514
+ with open(Path(__file__), "w") as f:
515
+ f.write(updated_content)
516
+
517
+ logger.info(f"Created new command: {command_name} in self module")
518
+ click.echo(f"Created new command: {command_name} in self module")
519
+ click.echo(f"File created: {command_file_path}")
520
+ click.echo(f"Command has been automatically registered with self_app")
521
+
522
+ return 0
523
+
524
+
525
+ @click.group("plugin")
526
+ def plugin():
527
+ """
528
+ Manage plugins for mcli.
529
+
530
+ Use one of the subcommands: add, remove, update.
531
+ """
532
+ logger.info("Plugin management commands loaded")
533
+ pass
534
+
535
+
536
+ @plugin.command("add")
537
+ @click.argument("plugin_name")
538
+ @click.argument("repo_url", required=False)
539
+ def plugin_add(plugin_name, repo_url=None):
540
+ """Add a new plugin."""
541
+ # First, check for config path in environment variable
542
+ logger.info(f"Adding plugin: {plugin_name} with repo URL: {repo_url}")
543
+ config_env = os.environ.get("MCLI_CONFIG")
544
+ config_path = None
545
+
546
+ if config_env and Path(config_env).expanduser().exists():
547
+ config_path = Path(config_env).expanduser()
548
+ else:
549
+ # Default to $HOME/.config/mcli/config.toml
550
+ home_config = Path.home() / ".config" / "mcli" / "config.toml"
551
+ if home_config.exists():
552
+ config_path = home_config
553
+ else:
554
+ # Fallback to top-level config.toml
555
+ top_level_config = Path(__file__).parent.parent.parent / "config.toml"
556
+ if top_level_config.exists():
557
+ config_path = top_level_config
558
+
559
+ if not config_path or not config_path.exists():
560
+ click.echo(
561
+ "Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
562
+ err=True,
563
+ )
564
+ return 1
565
+
566
+ with open(config_path, "rb") as f:
567
+ config = tomli.load(f)
568
+
569
+ # Example: plugins are listed under [plugins]
570
+ plugins = config.get("plugins", {})
571
+ if plugin_name in plugins:
572
+ click.echo(f"Plugin '{plugin_name}' already exists in config.toml.")
573
+ return 1
574
+
575
+ # Determine plugin install path
576
+ plugin_path = None
577
+ # 1. Check config file for plugin location
578
+ plugin_location = config.get("plugin_location")
579
+ if plugin_location:
580
+ plugin_path = Path(plugin_location).expanduser()
581
+ else:
582
+ # 2. Check env variable
583
+ env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
584
+ if env_plugin_path:
585
+ plugin_path = Path(env_plugin_path).expanduser()
586
+ else:
587
+ # 3. Default location
588
+ plugin_path = Path.home() / ".config" / "mcli" / "plugins"
589
+
590
+ plugin_path.mkdir(parents=True, exist_ok=True)
591
+
592
+ # Download the repo if a URL is provided
593
+ if repo_url:
594
+ import subprocess
595
+
596
+ dest = plugin_path / plugin_name
597
+ if dest.exists():
598
+ click.echo(f"Plugin directory already exists at {dest}. Aborting download.", err=True)
599
+ return 1
600
+ try:
601
+ click.echo(f"Cloning {repo_url} into {dest} ...")
602
+ subprocess.run(["git", "clone", repo_url, str(dest)], check=True)
603
+ click.echo(f"Plugin '{plugin_name}' cloned to {dest}")
604
+ except Exception as e:
605
+ click.echo(f"Failed to clone repository: {e}", err=True)
606
+ return 1
607
+ else:
608
+ click.echo("No repo URL provided, plugin will not be downloaded.")
609
+
610
+ # TODO: Optionally update config.toml to register the new plugin
611
+
612
+ return 0
613
+
614
+
615
+ @plugin.command("remove")
616
+ @click.argument("plugin_name")
617
+ def plugin_remove(plugin_name):
618
+ """Remove an existing plugin."""
619
+ # Determine plugin install path as in plugin_add
620
+ logger.info(f"Removing plugin: {plugin_name}")
621
+ config_env = os.environ.get("MCLI_CONFIG")
622
+ config_path = None
623
+
624
+ if config_env and Path(config_env).expanduser().exists():
625
+ config_path = Path(config_env).expanduser()
626
+ else:
627
+ home_config = Path.home() / ".config" / "mcli" / "config.toml"
628
+ if home_config.exists():
629
+ config_path = home_config
630
+ else:
631
+ top_level_config = Path(__file__).parent.parent.parent / "config.toml"
632
+ if top_level_config.exists():
633
+ config_path = top_level_config
634
+
635
+ if not config_path or not config_path.exists():
636
+ click.echo(
637
+ "Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
638
+ err=True,
639
+ )
640
+ return 1
641
+
642
+ with open(config_path, "rb") as f:
643
+ config = tomli.load(f)
644
+
645
+ plugin_location = config.get("plugin_location")
646
+ if plugin_location:
647
+ plugin_path = Path(plugin_location).expanduser()
648
+ else:
649
+ env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
650
+ if env_plugin_path:
651
+ plugin_path = Path(env_plugin_path).expanduser()
652
+ else:
653
+ plugin_path = Path.home() / ".config" / "mcli" / "plugins"
654
+
655
+ dest = plugin_path / plugin_name
656
+ if not dest.exists():
657
+ click.echo(f"Plugin directory does not exist at {dest}. Nothing to remove.", err=True)
658
+ return 1
659
+
660
+ import shutil
661
+
662
+ try:
663
+ shutil.rmtree(dest)
664
+ click.echo(f"Plugin '{plugin_name}' removed from {dest}")
665
+ except Exception as e:
666
+ click.echo(f"Failed to remove plugin: {e}", err=True)
667
+ return 1
668
+
669
+ # TODO: Optionally update config.toml to unregister the plugin
670
+
671
+ return 0
672
+
673
+
674
+ @plugin.command("update")
675
+ @click.argument("plugin_name")
676
+ def plugin_update(plugin_name):
677
+ """Update an existing plugin (git pull on default branch)."""
678
+ """Update an existing plugin by pulling the latest changes from its repository."""
679
+ # Determine plugin install path as in plugin_add
680
+ config_env = os.environ.get("MCLI_CONFIG")
681
+ config_path = None
682
+
683
+ # Determine plugin install path as in plugin_add
684
+ config_env = os.environ.get("MCLI_CONFIG")
685
+ config_path = None
686
+
687
+ if config_env and Path(config_env).expanduser().exists():
688
+ config_path = Path(config_env).expanduser()
689
+ else:
690
+ home_config = Path.home() / ".config" / "mcli" / "config.toml"
691
+ if home_config.exists():
692
+ config_path = home_config
693
+ else:
694
+ top_level_config = Path(__file__).parent.parent.parent / "config.toml"
695
+ if top_level_config.exists():
696
+ config_path = top_level_config
697
+
698
+ if not config_path or not config_path.exists():
699
+ click.echo(
700
+ "Config file not found in $MCLI_CONFIG, $HOME/.config/mcli/config.toml, or project root.",
701
+ err=True,
702
+ )
703
+ return 1
704
+
705
+ with open(config_path, "rb") as f:
706
+ config = tomli.load(f)
707
+
708
+ plugin_location = config.get("plugin_location")
709
+ if plugin_location:
710
+ plugin_path = Path(plugin_location).expanduser()
711
+ else:
712
+ env_plugin_path = os.environ.get("MCLI_PLUGIN_PATH")
713
+ if env_plugin_path:
714
+ plugin_path = Path(env_plugin_path).expanduser()
715
+ else:
716
+ plugin_path = Path.home() / ".config" / "mcli" / "plugins"
717
+
718
+ dest = plugin_path / plugin_name
719
+ if not dest.exists():
720
+ click.echo(f"Plugin directory does not exist at {dest}. Cannot update.", err=True)
721
+ return 1
722
+
723
+ import subprocess
724
+
725
+ try:
726
+ click.echo(f"Updating plugin '{plugin_name}' in {dest} ...")
727
+ subprocess.run(["git", "-C", str(dest), "pull"], check=True)
728
+ click.echo(f"Plugin '{plugin_name}' updated (git pull).")
729
+ except Exception as e:
730
+ click.echo(f"Failed to update plugin: {e}", err=True)
731
+ return 1
732
+
733
+ return 0
734
+
735
+
736
+ @self_app.command("hello")
737
+ @click.argument("name", default="World")
738
+ def hello(name: str):
739
+ """A simple hello command for testing."""
740
+ message = f"Hello, {name}! This is the MCLI hello command."
741
+ logger.info(message)
742
+ console.print(f"[green]{message}[/green]")
743
+
744
+
745
+ @self_app.command("logs")
746
+ @click.option(
747
+ "--type",
748
+ "-t",
749
+ type=click.Choice(["main", "system", "trace", "all"]),
750
+ default="main",
751
+ help="Type of logs to display",
752
+ )
753
+ @click.option("--lines", "-n", default=50, help="Number of lines to show (default: 50)")
754
+ @click.option("--follow", "-f", is_flag=True, help="Follow log output in real-time")
755
+ @click.option("--date", "-d", help="Show logs for specific date (YYYYMMDD format)")
756
+ @click.option("--grep", "-g", help="Filter logs by pattern")
757
+ @click.option(
758
+ "--level",
759
+ "-l",
760
+ type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
761
+ help="Filter logs by minimum level",
762
+ )
763
+ def logs(type: str, lines: int, follow: bool, date: str, grep: str, level: str):
764
+ """
765
+ Display runtime logs of the mcli application.
766
+
767
+ Shows the most recent log entries from the application's logging system.
768
+ Supports filtering by log type, date, content, and log level.
769
+
770
+ Log files are named as mcli_YYYYMMDD.log, mcli_system_YYYYMMDD.log, mcli_trace_YYYYMMDD.log.
771
+ """
772
+ import re
773
+ import subprocess
774
+ from datetime import datetime
775
+ from pathlib import Path
776
+
777
+ # Find the logs directory
778
+ current_file = Path(__file__)
779
+ # Go up 5 levels: file -> self -> mcli -> src -> repo_root
780
+ project_root = current_file.parents[4]
781
+ logs_dir = project_root / "logs"
782
+
783
+ # Alternative: try current working directory first
784
+ if not logs_dir.exists():
785
+ logs_dir = Path.cwd() / "logs"
786
+
787
+ if not logs_dir.exists():
788
+ click.echo("❌ Logs directory not found", err=True)
789
+ return
790
+
791
+ # Determine which log files to read
792
+ log_files = []
793
+
794
+ if type == "all":
795
+ # Get all log files for the specified date or latest
796
+ if date:
797
+ # Look for files like mcli_20250709.log, mcli_system_20250709.log, mcli_trace_20250709.log
798
+ patterns = [f"mcli_{date}.log", f"mcli_system_{date}.log", f"mcli_trace_{date}.log"]
799
+ else:
800
+ # Get the most recent log files
801
+ patterns = ["mcli_*.log"]
802
+
803
+ log_files = []
804
+ for pattern in patterns:
805
+ files = list(logs_dir.glob(pattern))
806
+ if files:
807
+ # Sort by modification time (newest first)
808
+ files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
809
+ log_files.extend(files)
810
+
811
+ # Remove duplicates and take only the most recent files of each type
812
+ seen_types = set()
813
+ filtered_files = []
814
+ for log_file in log_files:
815
+ # Extract log type from filename
816
+ # mcli_20250709 -> main
817
+ # mcli_system_20250709 -> system
818
+ # mcli_trace_20250709 -> trace
819
+ if log_file.name.startswith("mcli_system_"):
820
+ log_type = "system"
821
+ elif log_file.name.startswith("mcli_trace_"):
822
+ log_type = "trace"
823
+ else:
824
+ log_type = "main"
825
+
826
+ if log_type not in seen_types:
827
+ seen_types.add(log_type)
828
+ filtered_files.append(log_file)
829
+
830
+ log_files = filtered_files
831
+ else:
832
+ # Get specific log type
833
+ if date:
834
+ if type == "main":
835
+ filename = f"mcli_{date}.log"
836
+ else:
837
+ filename = f"mcli_{type}_{date}.log"
838
+ else:
839
+ # Find the most recent file for this type
840
+ if type == "main":
841
+ pattern = "mcli_*.log"
842
+ # Exclude system and trace files
843
+ exclude_patterns = ["mcli_system_*.log", "mcli_trace_*.log"]
844
+ else:
845
+ pattern = f"mcli_{type}_*.log"
846
+ exclude_patterns = []
847
+
848
+ files = list(logs_dir.glob(pattern))
849
+
850
+ # Filter out excluded patterns
851
+ if exclude_patterns:
852
+ filtered_files = []
853
+ for file in files:
854
+ excluded = False
855
+ for exclude_pattern in exclude_patterns:
856
+ if file.match(exclude_pattern):
857
+ excluded = True
858
+ break
859
+ if not excluded:
860
+ filtered_files.append(file)
861
+ files = filtered_files
862
+
863
+ if files:
864
+ # Sort by modification time and take the most recent
865
+ files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
866
+ filename = files[0].name
867
+ else:
868
+ click.echo(f"❌ No {type} log files found", err=True)
869
+ return
870
+
871
+ log_file = logs_dir / filename
872
+ if log_file.exists():
873
+ log_files = [log_file]
874
+ else:
875
+ click.echo(f"❌ Log file not found: {filename}", err=True)
876
+ return
877
+
878
+ if not log_files:
879
+ click.echo("❌ No log files found", err=True)
880
+ return
881
+
882
+ # Display log file information
883
+ click.echo(f"📋 Showing logs from {len(log_files)} file(s):")
884
+ for log_file in log_files:
885
+ size_mb = log_file.stat().st_size / (1024 * 1024)
886
+ modified = datetime.fromtimestamp(log_file.stat().st_mtime)
887
+ click.echo(
888
+ f" 📄 {log_file.name} ({size_mb:.1f}MB, modified {modified.strftime('%Y-%m-%d %H:%M:%S')})"
889
+ )
890
+ click.echo()
891
+
892
+ # Process each log file
893
+ for log_file in log_files:
894
+ click.echo(f"🔍 Reading: {log_file.name}")
895
+ click.echo("─" * 80)
896
+
897
+ try:
898
+ # Read the file content
899
+ with open(log_file, "r") as f:
900
+ content = f.readlines()
901
+
902
+ # Apply filters
903
+ filtered_lines = []
904
+ for line in content:
905
+ # Apply grep filter
906
+ if grep and grep.lower() not in line.lower():
907
+ continue
908
+
909
+ # Apply level filter
910
+ if level:
911
+ level_pattern = rf"\b{level}\b"
912
+ if not re.search(level_pattern, line, re.IGNORECASE):
913
+ # Check if line has a lower level than requested
914
+ level_order = {
915
+ "DEBUG": 0,
916
+ "INFO": 1,
917
+ "WARNING": 2,
918
+ "ERROR": 3,
919
+ "CRITICAL": 4,
920
+ }
921
+ requested_level = level_order.get(level.upper(), 0)
922
+
923
+ # Check if line contains any log level
924
+ found_level = None
925
+ for log_level in level_order:
926
+ if log_level in line.upper():
927
+ found_level = level_order[log_level]
928
+ break
929
+
930
+ if found_level is None or found_level < requested_level:
931
+ continue
932
+
933
+ filtered_lines.append(line)
934
+
935
+ # Show the last N lines
936
+ if lines > 0:
937
+ filtered_lines = filtered_lines[-lines:]
938
+
939
+ # Display the lines
940
+ for line in filtered_lines:
941
+ # Colorize log levels
942
+ colored_line = line
943
+ if "ERROR" in line or "CRITICAL" in line:
944
+ colored_line = click.style(line, fg="red")
945
+ elif "WARNING" in line:
946
+ colored_line = click.style(line, fg="yellow")
947
+ elif "INFO" in line:
948
+ colored_line = click.style(line, fg="green")
949
+ elif "DEBUG" in line:
950
+ colored_line = click.style(line, fg="blue")
951
+
952
+ click.echo(colored_line.rstrip())
953
+
954
+ if not filtered_lines:
955
+ click.echo("(No matching log entries found)")
956
+
957
+ except Exception as e:
958
+ click.echo(f"❌ Error reading log file {log_file.name}: {e}", err=True)
959
+
960
+ click.echo()
961
+
962
+ if follow:
963
+ click.echo("🔄 Following log output... (Press Ctrl+C to stop)")
964
+ try:
965
+ # Use tail -f for real-time following
966
+ for log_file in log_files:
967
+ click.echo(f"📡 Following: {log_file.name}")
968
+ process = subprocess.Popen(
969
+ ["tail", "-f", str(log_file)],
970
+ stdout=subprocess.PIPE,
971
+ stderr=subprocess.PIPE,
972
+ text=True,
973
+ )
974
+
975
+ try:
976
+ if process.stdout:
977
+ for line in process.stdout:
978
+ # Apply filters to real-time output
979
+ if grep and grep.lower() not in line.lower():
980
+ continue
981
+
982
+ if level:
983
+ level_pattern = rf"\b{level}\b"
984
+ if not re.search(level_pattern, line, re.IGNORECASE):
985
+ continue
986
+
987
+ # Colorize and display
988
+ colored_line = line
989
+ if "ERROR" in line or "CRITICAL" in line:
990
+ colored_line = click.style(line, fg="red")
991
+ elif "WARNING" in line:
992
+ colored_line = click.style(line, fg="yellow")
993
+ elif "INFO" in line:
994
+ colored_line = click.style(line, fg="green")
995
+ elif "DEBUG" in line:
996
+ colored_line = click.style(line, fg="blue")
997
+
998
+ click.echo(colored_line.rstrip())
999
+
1000
+ except KeyboardInterrupt:
1001
+ process.terminate()
1002
+ break
1003
+
1004
+ except KeyboardInterrupt:
1005
+ click.echo("\n🛑 Stopped following logs")
1006
+ except Exception as e:
1007
+ click.echo(f"❌ Error following logs: {e}", err=True)
1008
+
1009
+
1010
+ @self_app.command("performance")
1011
+ @click.option("--detailed", "-d", is_flag=True, help="Show detailed performance information")
1012
+ @click.option("--benchmark", "-b", is_flag=True, help="Run performance benchmarks")
1013
+ def performance(detailed: bool, benchmark: bool):
1014
+ """🚀 Show performance optimization status and benchmarks"""
1015
+ try:
1016
+ from mcli.lib.performance.optimizer import get_global_optimizer
1017
+ from mcli.lib.performance.rust_bridge import print_performance_summary
1018
+
1019
+ # Always show the performance summary
1020
+ print_performance_summary()
1021
+
1022
+ if detailed:
1023
+ console.print("\n📊 Detailed Performance Information:")
1024
+ console.print("─" * 60)
1025
+
1026
+ optimizer = get_global_optimizer()
1027
+ summary = optimizer.get_optimization_summary()
1028
+
1029
+ table = Table(
1030
+ title="Detailed Optimization Results", show_header=True, header_style="bold magenta"
1031
+ )
1032
+ table.add_column("Optimization", style="cyan", width=20)
1033
+ table.add_column("Status", justify="center", width=10)
1034
+ table.add_column("Details", style="white", width=40)
1035
+
1036
+ for name, details in summary["details"].items():
1037
+ status = "✅" if details.get("success") else "❌"
1038
+ detail_text = details.get("performance_gain", "N/A")
1039
+ if details.get("optimizations"):
1040
+ opts = details["optimizations"]
1041
+ detail_text += f"\n{len(opts)} optimizations applied"
1042
+
1043
+ table.add_row(name.replace("_", " ").title(), status, detail_text)
1044
+
1045
+ console.print(table)
1046
+
1047
+ console.print(
1048
+ f"\nđŸŽ¯ Estimated Performance Gain: {summary['estimated_performance_gain']}"
1049
+ )
1050
+
1051
+ if benchmark:
1052
+ console.print("\n🏁 Running Performance Benchmarks...")
1053
+ console.print("─" * 60)
1054
+
1055
+ try:
1056
+ from mcli.lib.ui.visual_effects import MCLIProgressBar
1057
+
1058
+ progress = MCLIProgressBar.create_fancy_progress()
1059
+ with progress:
1060
+ # Benchmark task
1061
+ task = progress.add_task("đŸ”Ĩ Running TF-IDF benchmark...", total=100)
1062
+
1063
+ optimizer = get_global_optimizer()
1064
+
1065
+ # Update progress
1066
+ for i in range(20):
1067
+ progress.update(task, advance=5)
1068
+ time.sleep(0.05)
1069
+
1070
+ # Run actual benchmark
1071
+ benchmark_results = optimizer.benchmark_performance("medium")
1072
+
1073
+ progress.update(task, advance=100)
1074
+
1075
+ # Display results
1076
+ if benchmark_results:
1077
+ console.print("\n📈 Benchmark Results:")
1078
+
1079
+ tfidf_results = benchmark_results.get("tfidf_benchmark", {})
1080
+ if tfidf_results.get("rust") and tfidf_results.get("python"):
1081
+ speedup = tfidf_results["python"] / tfidf_results["rust"]
1082
+ console.print(f" đŸĻ€ Rust TF-IDF: {tfidf_results['rust']:.3f}s")
1083
+ console.print(f" 🐍 Python TF-IDF: {tfidf_results['python']:.3f}s")
1084
+ console.print(f" ⚡ Speedup: {speedup:.1f}x faster with Rust!")
1085
+
1086
+ system_info = benchmark_results.get("system_info", {})
1087
+ if system_info:
1088
+ console.print(f"\nđŸ’ģ System Info:")
1089
+ console.print(f" Platform: {system_info.get('platform', 'Unknown')}")
1090
+ console.print(f" CPUs: {system_info.get('cpu_count', 'Unknown')}")
1091
+ console.print(
1092
+ f" Memory: {system_info.get('memory_total', 0) // (1024**3):.1f}GB"
1093
+ )
1094
+
1095
+ except ImportError:
1096
+ click.echo("📊 Benchmark functionality requires additional dependencies")
1097
+ click.echo("💡 Install with: pip install rich")
1098
+
1099
+ except ImportError as e:
1100
+ click.echo(f"❌ Performance monitoring not available: {e}")
1101
+ click.echo("💡 Try installing dependencies: pip install rich psutil")
1102
+ except Exception as e:
1103
+ click.echo(f"❌ Error showing performance status: {e}")
1104
+
1105
+
1106
+ @self_app.command()
1107
+ @click.option("--refresh", "-r", default=2.0, help="Refresh interval in seconds")
1108
+ @click.option("--once", is_flag=True, help="Show dashboard once and exit")
1109
+ def dashboard(refresh: float, once: bool):
1110
+ """📊 Launch live system dashboard"""
1111
+ try:
1112
+ from mcli.lib.ui.visual_effects import LiveDashboard
1113
+
1114
+ dashboard = LiveDashboard()
1115
+
1116
+ if once:
1117
+ # Show dashboard once
1118
+ console.clear()
1119
+ layout = dashboard.create_full_dashboard()
1120
+ console.print(layout)
1121
+ else:
1122
+ # Start live updating dashboard
1123
+ dashboard.start_live_dashboard(refresh_interval=refresh)
1124
+
1125
+ except ImportError as e:
1126
+ console.print("[red]Dashboard module not available[/red]")
1127
+ console.print(f"Error: {e}")
1128
+ except Exception as e:
1129
+ console.print(f"[red]Error launching dashboard: {e}[/red]")
1130
+
1131
+
1132
+ @self_app.command()
1133
+ @click.option("--check", is_flag=True, help="Only check for updates, don't install")
1134
+ @click.option("--pre", is_flag=True, help="Include pre-release versions")
1135
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
1136
+ def update(check: bool, pre: bool, yes: bool):
1137
+ """🔄 Check for and install mcli updates from PyPI"""
1138
+ import subprocess
1139
+ import sys
1140
+ from importlib.metadata import version as get_version
1141
+
1142
+ try:
1143
+ import requests
1144
+ except ImportError:
1145
+ console.print("[red]❌ Error: 'requests' module not found[/red]")
1146
+ console.print("[yellow]Install it with: pip install requests[/yellow]")
1147
+ return
1148
+
1149
+ try:
1150
+ # Get current version
1151
+ try:
1152
+ current_version = get_version("mcli-framework")
1153
+ except Exception:
1154
+ console.print("[yellow]âš ī¸ Could not determine current version[/yellow]")
1155
+ current_version = "unknown"
1156
+
1157
+ console.print(f"[cyan]Current version:[/cyan] {current_version}")
1158
+ console.print("[cyan]Checking PyPI for updates...[/cyan]")
1159
+
1160
+ # Check PyPI for latest version
1161
+ try:
1162
+ response = requests.get("https://pypi.org/pypi/mcli-framework/json", timeout=10)
1163
+ response.raise_for_status()
1164
+ pypi_data = response.json()
1165
+ except requests.RequestException as e:
1166
+ console.print(f"[red]❌ Error fetching version info from PyPI: {e}[/red]")
1167
+ return
1168
+
1169
+ # Get latest version
1170
+ if pre:
1171
+ # Include pre-releases
1172
+ all_versions = list(pypi_data["releases"].keys())
1173
+ latest_version = max(all_versions, key=lambda v: [int(x) for x in v.split(".")] if v[0].isdigit() else [0])
1174
+ else:
1175
+ # Only stable releases
1176
+ latest_version = pypi_data["info"]["version"]
1177
+
1178
+ console.print(f"[cyan]Latest version:[/cyan] {latest_version}")
1179
+
1180
+ # Compare versions
1181
+ if current_version == latest_version:
1182
+ console.print("[green]✅ You're already on the latest version![/green]")
1183
+ return
1184
+
1185
+ # Parse versions for comparison
1186
+ def parse_version(v):
1187
+ try:
1188
+ return tuple(int(x) for x in v.split(".") if x.isdigit())
1189
+ except:
1190
+ return (0, 0, 0)
1191
+
1192
+ current_parsed = parse_version(current_version)
1193
+ latest_parsed = parse_version(latest_version)
1194
+
1195
+ if current_parsed >= latest_parsed:
1196
+ console.print(f"[green]✅ Your version ({current_version}) is up to date or newer[/green]")
1197
+ return
1198
+
1199
+ console.print(f"[yellow]âŦ†ī¸ Update available: {current_version} → {latest_version}[/yellow]")
1200
+
1201
+ # Show release notes if available
1202
+ if "urls" in pypi_data["info"] and pypi_data["info"].get("project_urls"):
1203
+ project_urls = pypi_data["info"]["project_urls"]
1204
+ if "Changelog" in project_urls:
1205
+ console.print(f"[dim]📝 Changelog: {project_urls['Changelog']}[/dim]")
1206
+
1207
+ if check:
1208
+ console.print("[cyan]â„šī¸ Run 'mcli self update' to install the update[/cyan]")
1209
+ return
1210
+
1211
+ # Ask for confirmation unless --yes flag is used
1212
+ if not yes:
1213
+ from rich.prompt import Confirm
1214
+ if not Confirm.ask(f"[yellow]Install mcli {latest_version}?[/yellow]"):
1215
+ console.print("[yellow]Update cancelled[/yellow]")
1216
+ return
1217
+
1218
+ # Install update
1219
+ console.print(f"[cyan]đŸ“Ļ Installing mcli {latest_version}...[/cyan]")
1220
+
1221
+ # Use pip to upgrade
1222
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "mcli-framework"]
1223
+ if pre:
1224
+ cmd.append("--pre")
1225
+
1226
+ result = subprocess.run(cmd, capture_output=True, text=True)
1227
+
1228
+ if result.returncode == 0:
1229
+ console.print(f"[green]✅ Successfully updated to mcli {latest_version}![/green]")
1230
+ console.print("[yellow]â„šī¸ Restart your terminal or run 'hash -r' to use the new version[/yellow]")
1231
+ else:
1232
+ console.print(f"[red]❌ Update failed:[/red]")
1233
+ console.print(result.stderr)
1234
+
1235
+ except Exception as e:
1236
+ console.print(f"[red]❌ Error during update: {e}[/red]")
1237
+ import traceback
1238
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
1239
+
1240
+
1241
+ # Register the plugin group with self_app
1242
+ self_app.add_command(plugin)
1243
+
1244
+ # This part is important to make the command available to the CLI
1245
+ if __name__ == "__main__":
1246
+ self_app()