mcli-framework 7.3.1__py3-none-any.whl → 7.5.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 (96) hide show
  1. mcli/app/commands_cmd.py +741 -0
  2. mcli/lib/auth/aws_manager.py +9 -64
  3. mcli/lib/auth/azure_manager.py +9 -64
  4. mcli/lib/auth/credential_manager.py +70 -1
  5. mcli/lib/auth/gcp_manager.py +11 -64
  6. mcli/ml/dashboard/app.py +6 -39
  7. mcli/ml/dashboard/app_integrated.py +288 -117
  8. mcli/ml/dashboard/app_supabase.py +8 -57
  9. mcli/ml/dashboard/app_training.py +10 -12
  10. mcli/ml/dashboard/common.py +167 -0
  11. mcli/ml/dashboard/overview.py +378 -0
  12. mcli/ml/dashboard/pages/cicd.py +4 -4
  13. mcli/ml/dashboard/pages/debug_dependencies.py +406 -0
  14. mcli/ml/dashboard/pages/gravity_viz.py +783 -0
  15. mcli/ml/dashboard/pages/monte_carlo_predictions.py +555 -0
  16. mcli/ml/dashboard/pages/predictions_enhanced.py +4 -2
  17. mcli/ml/dashboard/pages/scrapers_and_logs.py +25 -9
  18. mcli/ml/dashboard/pages/test_portfolio.py +54 -4
  19. mcli/ml/dashboard/pages/trading.py +80 -26
  20. mcli/ml/dashboard/streamlit_extras_utils.py +297 -0
  21. mcli/ml/dashboard/styles.py +55 -0
  22. mcli/ml/dashboard/utils.py +7 -0
  23. mcli/ml/dashboard/warning_suppression.py +34 -0
  24. mcli/ml/database/session.py +169 -16
  25. mcli/ml/predictions/monte_carlo.py +428 -0
  26. mcli/ml/trading/alpaca_client.py +82 -18
  27. mcli/self/self_cmd.py +182 -737
  28. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/METADATA +2 -3
  29. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/RECORD +33 -87
  30. mcli/__init__.py +0 -160
  31. mcli/__main__.py +0 -14
  32. mcli/app/__init__.py +0 -23
  33. mcli/app/model/__init__.py +0 -0
  34. mcli/app/video/__init__.py +0 -5
  35. mcli/chat/__init__.py +0 -34
  36. mcli/lib/__init__.py +0 -0
  37. mcli/lib/api/__init__.py +0 -0
  38. mcli/lib/auth/__init__.py +0 -1
  39. mcli/lib/config/__init__.py +0 -1
  40. mcli/lib/erd/__init__.py +0 -25
  41. mcli/lib/files/__init__.py +0 -0
  42. mcli/lib/fs/__init__.py +0 -1
  43. mcli/lib/logger/__init__.py +0 -3
  44. mcli/lib/performance/__init__.py +0 -17
  45. mcli/lib/pickles/__init__.py +0 -1
  46. mcli/lib/shell/__init__.py +0 -0
  47. mcli/lib/toml/__init__.py +0 -1
  48. mcli/lib/watcher/__init__.py +0 -0
  49. mcli/ml/__init__.py +0 -16
  50. mcli/ml/api/__init__.py +0 -30
  51. mcli/ml/api/routers/__init__.py +0 -27
  52. mcli/ml/auth/__init__.py +0 -45
  53. mcli/ml/backtesting/__init__.py +0 -39
  54. mcli/ml/cli/__init__.py +0 -5
  55. mcli/ml/config/__init__.py +0 -33
  56. mcli/ml/configs/__init__.py +0 -16
  57. mcli/ml/dashboard/__init__.py +0 -12
  58. mcli/ml/dashboard/components/__init__.py +0 -7
  59. mcli/ml/dashboard/pages/__init__.py +0 -6
  60. mcli/ml/data_ingestion/__init__.py +0 -39
  61. mcli/ml/database/__init__.py +0 -47
  62. mcli/ml/experimentation/__init__.py +0 -29
  63. mcli/ml/features/__init__.py +0 -39
  64. mcli/ml/mlops/__init__.py +0 -33
  65. mcli/ml/models/__init__.py +0 -94
  66. mcli/ml/monitoring/__init__.py +0 -25
  67. mcli/ml/optimization/__init__.py +0 -27
  68. mcli/ml/predictions/__init__.py +0 -5
  69. mcli/ml/preprocessing/__init__.py +0 -28
  70. mcli/ml/scripts/__init__.py +0 -1
  71. mcli/ml/trading/__init__.py +0 -60
  72. mcli/ml/training/__init__.py +0 -10
  73. mcli/mygroup/__init__.py +0 -3
  74. mcli/public/__init__.py +0 -1
  75. mcli/public/commands/__init__.py +0 -2
  76. mcli/self/__init__.py +0 -3
  77. mcli/workflow/__init__.py +0 -0
  78. mcli/workflow/daemon/__init__.py +0 -15
  79. mcli/workflow/dashboard/__init__.py +0 -5
  80. mcli/workflow/docker/__init__.py +0 -0
  81. mcli/workflow/file/__init__.py +0 -0
  82. mcli/workflow/gcloud/__init__.py +0 -1
  83. mcli/workflow/git_commit/__init__.py +0 -0
  84. mcli/workflow/interview/__init__.py +0 -0
  85. mcli/workflow/politician_trading/__init__.py +0 -4
  86. mcli/workflow/registry/__init__.py +0 -0
  87. mcli/workflow/repo/__init__.py +0 -0
  88. mcli/workflow/scheduler/__init__.py +0 -25
  89. mcli/workflow/search/__init__.py +0 -0
  90. mcli/workflow/sync/__init__.py +0 -5
  91. mcli/workflow/videos/__init__.py +0 -1
  92. mcli/workflow/wakatime/__init__.py +0 -80
  93. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/WHEEL +0 -0
  94. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/entry_points.txt +0 -0
  95. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/licenses/LICENSE +0 -0
  96. {mcli_framework-7.3.1.dist-info → mcli_framework-7.5.0.dist-info}/top_level.txt +0 -0
mcli/self/self_cmd.py CHANGED
@@ -344,8 +344,38 @@ def collect_commands() -> List[Dict[str, Any]]:
344
344
  module_name = ".".join(relative_path.with_suffix("").parts)
345
345
 
346
346
  try:
347
- # Try to import the module
348
- module = importlib.import_module(module_name)
347
+ # Suppress Streamlit warnings and logging during module import
348
+ import warnings
349
+ import logging
350
+ import sys
351
+ import os
352
+ from contextlib import redirect_stderr
353
+ from io import StringIO
354
+
355
+ # Suppress Python warnings
356
+ with warnings.catch_warnings():
357
+ warnings.filterwarnings("ignore", message=".*missing ScriptRunContext.*")
358
+ warnings.filterwarnings("ignore", message=".*No runtime found.*")
359
+ warnings.filterwarnings("ignore", message=".*Session state does not function.*")
360
+ warnings.filterwarnings("ignore", message=".*to view this Streamlit app.*")
361
+
362
+ # Suppress Streamlit logger warnings
363
+ streamlit_logger = logging.getLogger("streamlit")
364
+ original_level = streamlit_logger.level
365
+ streamlit_logger.setLevel(logging.CRITICAL)
366
+
367
+ # Also suppress specific Streamlit sub-loggers
368
+ logging.getLogger("streamlit.runtime.scriptrunner_utils.script_run_context").setLevel(logging.CRITICAL)
369
+ logging.getLogger("streamlit.runtime.caching.cache_data_api").setLevel(logging.CRITICAL)
370
+
371
+ # Redirect stderr to suppress Streamlit warnings
372
+ with redirect_stderr(StringIO()):
373
+ try:
374
+ # Try to import the module
375
+ module = importlib.import_module(module_name)
376
+ finally:
377
+ # Restore original logging level
378
+ streamlit_logger.setLevel(original_level)
349
379
 
350
380
  # Extract command and group objects
351
381
  for name, obj in inspect.getmembers(module):
@@ -387,263 +417,150 @@ def collect_commands() -> List[Dict[str, Any]]:
387
417
  return commands
388
418
 
389
419
 
390
- @self_app.command("add-command")
391
- @click.argument("command_name", required=True)
392
- @click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
393
- @click.option(
394
- "--description", "-d", help="Description for the command", default="Custom command"
395
- )
396
- def add_command(command_name, group, description):
397
- """
398
- Generate a new portable custom command saved to ~/.mcli/commands/.
399
-
400
- Commands are automatically nested under the 'workflow' group by default,
401
- making them portable and persistent across updates.
402
-
403
- Example:
404
- mcli self add-command my_command
405
- mcli self add-command analytics --group data
406
- """
407
- command_name = command_name.lower().replace("-", "_")
408
-
409
- # Validate command name
410
- if not re.match(r"^[a-z][a-z0-9_]*$", command_name):
411
- logger.error(
412
- f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter)."
413
- )
414
- click.echo(
415
- f"Invalid command name: {command_name}. Use lowercase letters, numbers, and underscores (starting with a letter).",
416
- err=True,
417
- )
418
- return 1
419
-
420
- # Validate group name if provided
421
- if group:
422
- command_group = group.lower().replace("-", "_")
423
- if not re.match(r"^[a-z][a-z0-9_]*$", command_group):
424
- logger.error(
425
- f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter)."
426
- )
427
- click.echo(
428
- f"Invalid group name: {command_group}. Use lowercase letters, numbers, and underscores (starting with a letter).",
429
- err=True,
430
- )
431
- return 1
432
- else:
433
- command_group = "workflow" # Default to workflow group
434
-
435
- # Get the command manager
436
- manager = get_command_manager()
437
-
438
- # Check if command already exists
439
- command_file = manager.commands_dir / f"{command_name}.json"
440
- if command_file.exists():
441
- logger.warning(f"Custom command already exists: {command_name}")
442
- should_override = Prompt.ask(
443
- "Command already exists. Override?", choices=["y", "n"], default="n"
444
- )
445
- if should_override.lower() != "y":
446
- logger.info("Command creation aborted.")
447
- click.echo("Command creation aborted.")
448
- return 1
449
-
450
- # Generate command code
451
- code = get_command_template(command_name, command_group)
452
-
453
- # Save the command
454
- saved_path = manager.save_command(
455
- name=command_name,
456
- code=code,
457
- description=description,
458
- group=command_group,
459
- )
460
-
461
- logger.info(f"Created portable custom command: {command_name}")
462
- click.echo(f"✅ Created portable custom command: {command_name}")
463
- click.echo(f"📁 Saved to: {saved_path}")
464
- click.echo(f"🔄 Command will be automatically loaded on next mcli startup")
465
- click.echo(
466
- f"💡 You can share this command by copying {saved_path} to another machine's ~/.mcli/commands/ directory"
467
- )
468
-
469
- return 0
470
-
471
-
472
- @self_app.command("list-commands")
473
- def list_commands():
474
- """
475
- List all custom commands stored in ~/.mcli/commands/.
476
- """
477
- manager = get_command_manager()
478
- commands = manager.load_all_commands()
479
-
480
- if not commands:
481
- click.echo("No custom commands found.")
482
- click.echo(
483
- f"Create one with: mcli self add-command <name>"
484
- )
485
- return 0
486
-
487
- table = Table(title="Custom Commands")
488
- table.add_column("Name", style="green")
489
- table.add_column("Group", style="blue")
490
- table.add_column("Description", style="yellow")
491
- table.add_column("Version", style="cyan")
492
- table.add_column("Updated", style="dim")
493
-
494
- for cmd in commands:
495
- table.add_row(
496
- cmd["name"],
497
- cmd.get("group", "-"),
498
- cmd.get("description", ""),
499
- cmd.get("version", "1.0"),
500
- cmd.get("updated_at", "")[:10] if cmd.get("updated_at") else "-",
501
- )
502
-
503
- console.print(table)
504
- click.echo(f"\n📁 Commands directory: {manager.commands_dir}")
505
- click.echo(f"🔒 Lockfile: {manager.lockfile_path}")
506
-
507
- return 0
508
-
509
-
510
- @self_app.command("remove-command")
511
- @click.argument("command_name", required=True)
512
- @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
513
- def remove_command(command_name, yes):
420
+ def open_editor_for_command(command_name: str, command_group: str, description: str) -> Optional[str]:
514
421
  """
515
- Remove a custom command from ~/.mcli/commands/.
422
+ Open the user's default editor to allow them to write command logic.
423
+
424
+ Args:
425
+ command_name: Name of the command
426
+ command_group: Group for the command
427
+ description: Description of the command
428
+
429
+ Returns:
430
+ The Python code written by the user, or None if cancelled
516
431
  """
517
- manager = get_command_manager()
518
- command_file = manager.commands_dir / f"{command_name}.json"
519
-
520
- if not command_file.exists():
521
- click.echo(f"❌ Command '{command_name}' not found.", err=True)
522
- return 1
523
-
524
- if not yes:
525
- should_delete = Prompt.ask(
526
- f"Delete command '{command_name}'?", choices=["y", "n"], default="n"
527
- )
528
- if should_delete.lower() != "y":
529
- click.echo("Deletion cancelled.")
530
- return 0
531
-
532
- if manager.delete_command(command_name):
533
- click.echo(f" Deleted custom command: {command_name}")
534
- return 0
535
- else:
536
- click.echo(f"❌ Failed to delete command: {command_name}", err=True)
537
- return 1
538
-
539
-
540
- @self_app.command("export-commands")
541
- @click.argument("export_file", type=click.Path(), required=False)
542
- def export_commands(export_file):
543
- """
544
- Export all custom commands to a JSON file.
545
-
546
- If no file is specified, exports to commands-export.json in current directory.
547
- """
548
- manager = get_command_manager()
549
-
550
- if not export_file:
551
- export_file = "commands-export.json"
552
-
553
- export_path = Path(export_file)
554
-
555
- if manager.export_commands(export_path):
556
- click.echo(f" Exported custom commands to: {export_path}")
557
- click.echo(
558
- f"💡 Import on another machine with: mcli self import-commands {export_path}"
559
- )
560
- return 0
561
- else:
562
- click.echo("❌ Failed to export commands.", err=True)
563
- return 1
564
-
565
-
566
- @self_app.command("import-commands")
567
- @click.argument("import_file", type=click.Path(exists=True), required=True)
568
- @click.option("--overwrite", is_flag=True, help="Overwrite existing commands")
569
- def import_commands(import_file, overwrite):
570
- """
571
- Import custom commands from a JSON file.
572
- """
573
- manager = get_command_manager()
574
- import_path = Path(import_file)
575
-
576
- results = manager.import_commands(import_path, overwrite=overwrite)
577
-
578
- success_count = sum(1 for v in results.values() if v)
579
- failed_count = len(results) - success_count
580
-
581
- if success_count > 0:
582
- click.echo(f"✅ Imported {success_count} command(s)")
583
-
584
- if failed_count > 0:
585
- click.echo(
586
- f"⚠️ Skipped {failed_count} command(s) (already exist, use --overwrite to replace)"
587
- )
588
- click.echo("Skipped commands:")
589
- for name, success in results.items():
590
- if not success:
591
- click.echo(f" - {name}")
592
-
593
- return 0
594
-
595
-
596
- @self_app.command("verify-commands")
597
- def verify_commands():
598
- """
599
- Verify that custom commands match the lockfile.
600
- """
601
- manager = get_command_manager()
602
-
603
- # First, ensure lockfile is up to date
604
- manager.update_lockfile()
605
-
606
- verification = manager.verify_lockfile()
607
-
608
- if verification["valid"]:
609
- click.echo("✅ All custom commands are in sync with the lockfile.")
610
- return 0
611
-
612
- click.echo("⚠️ Commands are out of sync with the lockfile:\n")
613
-
614
- if verification["missing"]:
615
- click.echo(f"Missing commands (in lockfile but not found):")
616
- for name in verification["missing"]:
617
- click.echo(f" - {name}")
618
-
619
- if verification["extra"]:
620
- click.echo(f"\nExtra commands (not in lockfile):")
621
- for name in verification["extra"]:
622
- click.echo(f" - {name}")
623
-
624
- if verification["modified"]:
625
- click.echo(f"\nModified commands:")
626
- for name in verification["modified"]:
627
- click.echo(f" - {name}")
628
-
629
- click.echo(f"\n💡 Run 'mcli self update-lockfile' to sync the lockfile")
432
+ import tempfile
433
+ import subprocess
434
+ import os
435
+ import sys
436
+ from pathlib import Path
437
+
438
+ # Get the user's default editor
439
+ editor = os.environ.get('EDITOR')
440
+ if not editor:
441
+ # Try common editors in order of preference
442
+ for common_editor in ['vim', 'nano', 'code', 'subl', 'atom', 'emacs']:
443
+ if subprocess.run(['which', common_editor], capture_output=True).returncode == 0:
444
+ editor = common_editor
445
+ break
446
+
447
+ if not editor:
448
+ click.echo(" No editor found. Please set the EDITOR environment variable or install vim/nano.")
449
+ return None
450
+
451
+ # Create a temporary file with the template
452
+ template = get_command_template(command_name, command_group)
453
+
454
+ # Add helpful comments to the template
455
+ enhanced_template = f'''"""
456
+ {command_name} command for mcli.{command_group}.
457
+
458
+ Description: {description}
459
+
460
+ Instructions:
461
+ 1. Write your Python command logic below
462
+ 2. Use Click decorators for command definition
463
+ 3. Save and close the editor to create the command
464
+ 4. The command will be automatically converted to JSON format
465
+
466
+ Example Click command structure:
467
+ @click.command()
468
+ @click.argument('name', default='World')
469
+ def my_command(name):
470
+ \"\"\"My custom command.\"\"\"
471
+ click.echo(f"Hello, {{name}}!")
472
+ """
473
+ import click
474
+ from typing import Optional, List
475
+ from pathlib import Path
476
+ from mcli.lib.logger.logger import get_logger
630
477
 
631
- return 1
478
+ logger = get_logger()
632
479
 
480
+ # Write your command logic here:
481
+ # Replace this template with your actual command implementation
633
482
 
634
- @self_app.command("update-lockfile")
635
- def update_lockfile():
636
- """
637
- Update the commands lockfile with current state.
638
- """
639
- manager = get_command_manager()
483
+ {template.split('"""')[2].split('"""')[0] if '"""' in template else ''}
640
484
 
641
- if manager.update_lockfile():
642
- click.echo(f"✅ Updated lockfile: {manager.lockfile_path}")
643
- return 0
644
- else:
645
- click.echo("❌ Failed to update lockfile.", err=True)
646
- return 1
485
+ # Your command implementation goes here:
486
+ # Example:
487
+ # @click.command()
488
+ # @click.argument('name', default='World')
489
+ # def {command_name}_command(name):
490
+ # \"\"\"{description}\"\"\"
491
+ # logger.info(f"Executing {command_name} command with name: {{name}}")
492
+ # click.echo(f"Hello, {{name}}! This is the {command_name} command.")
493
+ '''
494
+
495
+ # Create temporary file
496
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as temp_file:
497
+ temp_file.write(enhanced_template)
498
+ temp_file_path = temp_file.name
499
+
500
+ try:
501
+ # Check if we're in an interactive environment
502
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
503
+ click.echo("❌ Editor requires an interactive terminal. Use --template flag for non-interactive mode.")
504
+ return None
505
+
506
+ # Open editor
507
+ click.echo(f"📝 Opening {editor} to edit command logic...")
508
+ click.echo("💡 Write your Python command logic and save the file to continue.")
509
+ click.echo("💡 Press Ctrl+C to cancel command creation.")
510
+
511
+ # Run the editor
512
+ result = subprocess.run([editor, temp_file_path], check=False)
513
+
514
+ if result.returncode != 0:
515
+ click.echo("❌ Editor exited with error. Command creation cancelled.")
516
+ return None
517
+
518
+ # Read the edited content
519
+ with open(temp_file_path, 'r') as f:
520
+ edited_code = f.read()
521
+
522
+ # Check if the file was actually edited (not just the template)
523
+ if edited_code.strip() == enhanced_template.strip():
524
+ click.echo("⚠️ No changes detected. Command creation cancelled.")
525
+ return None
526
+
527
+ # Extract the actual command code (remove the instructions)
528
+ lines = edited_code.split('\n')
529
+ code_lines = []
530
+ in_code_section = False
531
+
532
+ for line in lines:
533
+ if line.strip().startswith('# Your command implementation goes here:'):
534
+ in_code_section = True
535
+ continue
536
+ if in_code_section:
537
+ code_lines.append(line)
538
+
539
+ if not code_lines or not any(line.strip() for line in code_lines):
540
+ # Fallback: use the entire file content
541
+ code_lines = lines
542
+
543
+ final_code = '\n'.join(code_lines).strip()
544
+
545
+ if not final_code:
546
+ click.echo("❌ No command code found. Command creation cancelled.")
547
+ return None
548
+
549
+ click.echo("✅ Command code captured successfully!")
550
+ return final_code
551
+
552
+ except KeyboardInterrupt:
553
+ click.echo("\n❌ Command creation cancelled by user.")
554
+ return None
555
+ except Exception as e:
556
+ click.echo(f"❌ Error opening editor: {e}")
557
+ return None
558
+ finally:
559
+ # Clean up temporary file
560
+ try:
561
+ os.unlink(temp_file_path)
562
+ except OSError:
563
+ pass
647
564
 
648
565
 
649
566
  @self_app.command("extract-workflow-commands")
@@ -971,265 +888,21 @@ def hello(name: str):
971
888
 
972
889
 
973
890
  @self_app.command("logs")
974
- @click.option(
975
- "--type",
976
- "-t",
977
- type=click.Choice(["main", "system", "trace", "all"]),
978
- default="main",
979
- help="Type of logs to display",
980
- )
981
- @click.option("--lines", "-n", default=50, help="Number of lines to show (default: 50)")
982
- @click.option("--follow", "-f", is_flag=True, help="Follow log output in real-time")
983
- @click.option("--date", "-d", help="Show logs for specific date (YYYYMMDD format)")
984
- @click.option("--grep", "-g", help="Filter logs by pattern")
985
- @click.option(
986
- "--level",
987
- "-l",
988
- type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
989
- help="Filter logs by minimum level",
990
- )
991
- def logs(type: str, lines: int, follow: bool, date: str, grep: str, level: str):
891
+ def logs():
992
892
  """
993
- Display runtime logs of the mcli application.
893
+ [DEPRECATED] Display runtime logs - Use 'mcli logs' instead.
994
894
 
995
- Shows the most recent log entries from the application's logging system.
996
- Supports filtering by log type, date, content, and log level.
997
-
998
- Log files are named as mcli_YYYYMMDD.log, mcli_system_YYYYMMDD.log, mcli_trace_YYYYMMDD.log.
895
+ This command has been moved to 'mcli logs' with enhanced features.
999
896
  """
1000
- import re
1001
- import subprocess
1002
- from datetime import datetime
1003
- from pathlib import Path
1004
-
1005
- # Import get_logs_dir to get the correct logs directory
1006
- from mcli.lib.paths import get_logs_dir
1007
-
1008
- # Get the logs directory (creates it if it doesn't exist)
1009
- logs_dir = get_logs_dir()
1010
-
1011
- if not logs_dir.exists():
1012
- click.echo("❌ Logs directory not found", err=True)
1013
- click.echo(f"Expected location: {logs_dir}", err=True)
1014
- return
1015
-
1016
- # Determine which log files to read
1017
- log_files = []
1018
-
1019
- if type == "all":
1020
- # Get all log files for the specified date or latest
1021
- if date:
1022
- # Look for files like mcli_20250709.log, mcli_system_20250709.log, mcli_trace_20250709.log
1023
- patterns = [f"mcli_{date}.log", f"mcli_system_{date}.log", f"mcli_trace_{date}.log"]
1024
- else:
1025
- # Get the most recent log files
1026
- patterns = ["mcli_*.log"]
1027
-
1028
- log_files = []
1029
- for pattern in patterns:
1030
- files = list(logs_dir.glob(pattern))
1031
- if files:
1032
- # Sort by modification time (newest first)
1033
- files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
1034
- log_files.extend(files)
1035
-
1036
- # Remove duplicates and take only the most recent files of each type
1037
- seen_types = set()
1038
- filtered_files = []
1039
- for log_file in log_files:
1040
- # Extract log type from filename
1041
- # mcli_20250709 -> main
1042
- # mcli_system_20250709 -> system
1043
- # mcli_trace_20250709 -> trace
1044
- if log_file.name.startswith("mcli_system_"):
1045
- log_type = "system"
1046
- elif log_file.name.startswith("mcli_trace_"):
1047
- log_type = "trace"
1048
- else:
1049
- log_type = "main"
1050
-
1051
- if log_type not in seen_types:
1052
- seen_types.add(log_type)
1053
- filtered_files.append(log_file)
1054
-
1055
- log_files = filtered_files
1056
- else:
1057
- # Get specific log type
1058
- if date:
1059
- if type == "main":
1060
- filename = f"mcli_{date}.log"
1061
- else:
1062
- filename = f"mcli_{type}_{date}.log"
1063
- else:
1064
- # Find the most recent file for this type
1065
- if type == "main":
1066
- pattern = "mcli_*.log"
1067
- # Exclude system and trace files
1068
- exclude_patterns = ["mcli_system_*.log", "mcli_trace_*.log"]
1069
- else:
1070
- pattern = f"mcli_{type}_*.log"
1071
- exclude_patterns = []
1072
-
1073
- files = list(logs_dir.glob(pattern))
1074
-
1075
- # Filter out excluded patterns
1076
- if exclude_patterns:
1077
- filtered_files = []
1078
- for file in files:
1079
- excluded = False
1080
- for exclude_pattern in exclude_patterns:
1081
- if file.match(exclude_pattern):
1082
- excluded = True
1083
- break
1084
- if not excluded:
1085
- filtered_files.append(file)
1086
- files = filtered_files
1087
-
1088
- if files:
1089
- # Sort by modification time and take the most recent
1090
- files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
1091
- filename = files[0].name
1092
- else:
1093
- click.echo(f"❌ No {type} log files found", err=True)
1094
- return
1095
-
1096
- log_file = logs_dir / filename
1097
- if log_file.exists():
1098
- log_files = [log_file]
1099
- else:
1100
- click.echo(f"❌ Log file not found: {filename}", err=True)
1101
- return
1102
-
1103
- if not log_files:
1104
- click.echo("❌ No log files found", err=True)
1105
- return
1106
-
1107
- # Display log file information
1108
- click.echo(f"📋 Showing logs from {len(log_files)} file(s):")
1109
- for log_file in log_files:
1110
- size_mb = log_file.stat().st_size / (1024 * 1024)
1111
- modified = datetime.fromtimestamp(log_file.stat().st_mtime)
1112
- click.echo(
1113
- f" 📄 {log_file.name} ({size_mb:.1f}MB, modified {modified.strftime('%Y-%m-%d %H:%M:%S')})"
1114
- )
1115
- click.echo()
1116
-
1117
- # Process each log file
1118
- for log_file in log_files:
1119
- click.echo(f"🔍 Reading: {log_file.name}")
1120
- click.echo("─" * 80)
1121
-
1122
- try:
1123
- # Read the file content
1124
- with open(log_file, "r") as f:
1125
- content = f.readlines()
1126
-
1127
- # Apply filters
1128
- filtered_lines = []
1129
- for line in content:
1130
- # Apply grep filter
1131
- if grep and grep.lower() not in line.lower():
1132
- continue
1133
-
1134
- # Apply level filter
1135
- if level:
1136
- level_pattern = rf"\b{level}\b"
1137
- if not re.search(level_pattern, line, re.IGNORECASE):
1138
- # Check if line has a lower level than requested
1139
- level_order = {
1140
- "DEBUG": 0,
1141
- "INFO": 1,
1142
- "WARNING": 2,
1143
- "ERROR": 3,
1144
- "CRITICAL": 4,
1145
- }
1146
- requested_level = level_order.get(level.upper(), 0)
1147
-
1148
- # Check if line contains any log level
1149
- found_level = None
1150
- for log_level in level_order:
1151
- if log_level in line.upper():
1152
- found_level = level_order[log_level]
1153
- break
1154
-
1155
- if found_level is None or found_level < requested_level:
1156
- continue
1157
-
1158
- filtered_lines.append(line)
1159
-
1160
- # Show the last N lines
1161
- if lines > 0:
1162
- filtered_lines = filtered_lines[-lines:]
1163
-
1164
- # Display the lines
1165
- for line in filtered_lines:
1166
- # Colorize log levels
1167
- colored_line = line
1168
- if "ERROR" in line or "CRITICAL" in line:
1169
- colored_line = click.style(line, fg="red")
1170
- elif "WARNING" in line:
1171
- colored_line = click.style(line, fg="yellow")
1172
- elif "INFO" in line:
1173
- colored_line = click.style(line, fg="green")
1174
- elif "DEBUG" in line:
1175
- colored_line = click.style(line, fg="blue")
1176
-
1177
- click.echo(colored_line.rstrip())
1178
-
1179
- if not filtered_lines:
1180
- click.echo("(No matching log entries found)")
1181
-
1182
- except Exception as e:
1183
- click.echo(f"❌ Error reading log file {log_file.name}: {e}", err=True)
1184
-
1185
- click.echo()
1186
-
1187
- if follow:
1188
- click.echo("🔄 Following log output... (Press Ctrl+C to stop)")
1189
- try:
1190
- # Use tail -f for real-time following
1191
- for log_file in log_files:
1192
- click.echo(f"📡 Following: {log_file.name}")
1193
- process = subprocess.Popen(
1194
- ["tail", "-f", str(log_file)],
1195
- stdout=subprocess.PIPE,
1196
- stderr=subprocess.PIPE,
1197
- text=True,
1198
- )
1199
-
1200
- try:
1201
- if process.stdout:
1202
- for line in process.stdout:
1203
- # Apply filters to real-time output
1204
- if grep and grep.lower() not in line.lower():
1205
- continue
1206
-
1207
- if level:
1208
- level_pattern = rf"\b{level}\b"
1209
- if not re.search(level_pattern, line, re.IGNORECASE):
1210
- continue
1211
-
1212
- # Colorize and display
1213
- colored_line = line
1214
- if "ERROR" in line or "CRITICAL" in line:
1215
- colored_line = click.style(line, fg="red")
1216
- elif "WARNING" in line:
1217
- colored_line = click.style(line, fg="yellow")
1218
- elif "INFO" in line:
1219
- colored_line = click.style(line, fg="green")
1220
- elif "DEBUG" in line:
1221
- colored_line = click.style(line, fg="blue")
1222
-
1223
- click.echo(colored_line.rstrip())
1224
-
1225
- except KeyboardInterrupt:
1226
- process.terminate()
1227
- break
1228
-
1229
- except KeyboardInterrupt:
1230
- click.echo("\n🛑 Stopped following logs")
1231
- except Exception as e:
1232
- click.echo(f"❌ Error following logs: {e}", err=True)
897
+ console.print("\n[yellow]⚠️ DEPRECATED:[/yellow] This command has been moved.")
898
+ console.print("\n[cyan]New usage:[/cyan]")
899
+ console.print(" mcli logs [bold]stream[/bold] - Stream logs in real-time")
900
+ console.print(" mcli logs [bold]list[/bold] - List available log files")
901
+ console.print(" mcli logs [bold]tail[/bold] - Tail recent log entries")
902
+ console.print(" mcli logs [bold]grep[/bold] - Search in log files")
903
+ console.print(" mcli logs [bold]location[/bold] - Show logs directory")
904
+ console.print(" mcli logs [bold]clear[/bold] - Clear old log files")
905
+ console.print("\n[dim]Run 'mcli logs --help' for more information[/dim]\n")
1233
906
 
1234
907
 
1235
908
  @self_app.command("performance")
@@ -1557,234 +1230,6 @@ def update(check: bool, pre: bool, yes: bool, skip_ci_check: bool):
1557
1230
  console.print(f"[dim]{traceback.format_exc()}[/dim]")
1558
1231
 
1559
1232
 
1560
- @self_app.command("import-script")
1561
- @click.argument("script_path", type=click.Path(exists=True))
1562
- @click.option("--name", "-n", help="Command name (defaults to script filename)")
1563
- @click.option("--group", "-g", default="workflow", help="Command group")
1564
- @click.option("--description", "-d", help="Command description")
1565
- @click.option("--interactive", "-i", is_flag=True, help="Open in $EDITOR for review/editing")
1566
- def import_script(script_path, name, group, description, interactive):
1567
- """
1568
- Import a Python script as a portable JSON command.
1569
-
1570
- Converts a Python script into a JSON command that can be loaded
1571
- by mcli. The script should define Click commands.
1572
-
1573
- Examples:
1574
- mcli self import-script my_script.py
1575
- mcli self import-script my_script.py --name custom-cmd --interactive
1576
- """
1577
- import subprocess
1578
- import tempfile
1579
-
1580
- script_file = Path(script_path).resolve()
1581
-
1582
- if not script_file.exists():
1583
- click.echo(f"❌ Script not found: {script_file}", err=True)
1584
- return 1
1585
-
1586
- # Read the script content
1587
- try:
1588
- with open(script_file, 'r') as f:
1589
- code = f.read()
1590
- except Exception as e:
1591
- click.echo(f"❌ Failed to read script: {e}", err=True)
1592
- return 1
1593
-
1594
- # Determine command name
1595
- if not name:
1596
- name = script_file.stem.lower().replace("-", "_")
1597
-
1598
- # Validate command name
1599
- if not re.match(r"^[a-z][a-z0-9_]*$", name):
1600
- click.echo(f"❌ Invalid command name: {name}", err=True)
1601
- return 1
1602
-
1603
- # Interactive editing
1604
- if interactive:
1605
- editor = os.environ.get('EDITOR', 'vim')
1606
- click.echo(f"📝 Opening in {editor} for review...")
1607
-
1608
- # Create temp file with the code
1609
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
1610
- tmp.write(code)
1611
- tmp_path = tmp.name
1612
-
1613
- try:
1614
- subprocess.run([editor, tmp_path], check=True)
1615
-
1616
- # Read edited content
1617
- with open(tmp_path, 'r') as f:
1618
- code = f.read()
1619
- finally:
1620
- Path(tmp_path).unlink(missing_ok=True)
1621
-
1622
- # Get description
1623
- if not description:
1624
- # Try to extract from docstring
1625
- import ast
1626
- try:
1627
- tree = ast.parse(code)
1628
- description = ast.get_docstring(tree) or f"Imported from {script_file.name}"
1629
- except:
1630
- description = f"Imported from {script_file.name}"
1631
-
1632
- # Save as JSON command
1633
- manager = get_command_manager()
1634
-
1635
- saved_path = manager.save_command(
1636
- name=name,
1637
- code=code,
1638
- description=description,
1639
- group=group,
1640
- metadata={
1641
- "source": "import-script",
1642
- "original_file": str(script_file),
1643
- "imported_at": datetime.now().isoformat()
1644
- }
1645
- )
1646
-
1647
- click.echo(f"✅ Imported script as command: {name}")
1648
- click.echo(f"📁 Saved to: {saved_path}")
1649
- click.echo(f"🔄 Use with: mcli {group} {name}")
1650
- click.echo(f"💡 Command will be available after restart or reload")
1651
-
1652
- return 0
1653
-
1654
-
1655
- @self_app.command("export-script")
1656
- @click.argument("command_name")
1657
- @click.option("--output", "-o", type=click.Path(), help="Output file path")
1658
- @click.option("--standalone", "-s", is_flag=True, help="Make script standalone (add if __name__ == '__main__')")
1659
- def export_script(command_name, output, standalone):
1660
- """
1661
- Export a JSON command to a Python script.
1662
-
1663
- Converts a portable JSON command back to a standalone Python script
1664
- that can be edited and run independently.
1665
-
1666
- Examples:
1667
- mcli self export-script my-command
1668
- mcli self export-script my-command --output my_script.py --standalone
1669
- """
1670
- manager = get_command_manager()
1671
-
1672
- # Load the command
1673
- command_file = manager.commands_dir / f"{command_name}.json"
1674
- if not command_file.exists():
1675
- click.echo(f"❌ Command not found: {command_name}", err=True)
1676
- return 1
1677
-
1678
- try:
1679
- with open(command_file, 'r') as f:
1680
- command_data = json.load(f)
1681
- except Exception as e:
1682
- click.echo(f"❌ Failed to load command: {e}", err=True)
1683
- return 1
1684
-
1685
- # Get the code
1686
- code = command_data.get('code', '')
1687
-
1688
- if not code:
1689
- click.echo(f"❌ Command has no code: {command_name}", err=True)
1690
- return 1
1691
-
1692
- # Add standalone wrapper if requested
1693
- if standalone:
1694
- # Check if already has if __name__ == '__main__'
1695
- if "if __name__" not in code:
1696
- code += "\n\nif __name__ == '__main__':\n app()\n"
1697
-
1698
- # Determine output path
1699
- if not output:
1700
- output = f"{command_name}.py"
1701
-
1702
- output_file = Path(output)
1703
-
1704
- # Write the script
1705
- try:
1706
- with open(output_file, 'w') as f:
1707
- f.write(code)
1708
- except Exception as e:
1709
- click.echo(f"❌ Failed to write script: {e}", err=True)
1710
- return 1
1711
-
1712
- click.echo(f"✅ Exported command to script: {output_file}")
1713
- click.echo(f"📝 Source command: {command_name}")
1714
-
1715
- if standalone:
1716
- click.echo(f"🚀 Run standalone with: python {output_file}")
1717
-
1718
- click.echo(f"💡 Edit and re-import with: mcli self import-script {output_file}")
1719
-
1720
- return 0
1721
-
1722
-
1723
- @self_app.command("edit-command")
1724
- @click.argument("command_name")
1725
- @click.option("--editor", "-e", help="Editor to use (defaults to $EDITOR)")
1726
- def edit_command(command_name, editor):
1727
- """
1728
- Edit a command interactively using $EDITOR.
1729
-
1730
- Opens the command's Python code in your preferred editor,
1731
- allows you to make changes, and saves the updated version.
1732
-
1733
- Examples:
1734
- mcli self edit-command my-command
1735
- mcli self edit-command my-command --editor code
1736
- """
1737
- import subprocess
1738
- import tempfile
1739
-
1740
- manager = get_command_manager()
1741
-
1742
- # Load the command
1743
- command_file = manager.commands_dir / f"{command_name}.json"
1744
- if not command_file.exists():
1745
- click.echo(f"❌ Command not found: {command_name}", err=True)
1746
- return 1
1747
-
1748
- try:
1749
- with open(command_file, 'r') as f:
1750
- command_data = json.load(f)
1751
- except Exception as e:
1752
- click.echo(f"❌ Failed to load command: {e}", err=True)
1753
- return 1
1754
-
1755
- code = command_data.get('code', '')
1756
-
1757
- if not code:
1758
- click.echo(f"❌ Command has no code: {command_name}", err=True)
1759
- return 1
1760
-
1761
- # Determine editor
1762
- if not editor:
1763
- editor = os.environ.get('EDITOR', 'vim')
1764
-
1765
- click.echo(f"📝 Opening command in {editor}...")
1766
-
1767
- # Create temp file with the code
1768
- with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False,
1769
- prefix=f"{command_name}_") as tmp:
1770
- tmp.write(code)
1771
- tmp_path = tmp.name
1772
-
1773
- try:
1774
- # Open in editor
1775
- result = subprocess.run([editor, tmp_path])
1776
-
1777
- if result.returncode != 0:
1778
- click.echo(f"⚠️ Editor exited with code {result.returncode}")
1779
-
1780
- # Read edited content
1781
- with open(tmp_path, 'r') as f:
1782
- new_code = f.read()
1783
-
1784
- # Check if code changed
1785
- if new_code.strip() == code.strip():
1786
- click.echo("ℹ️ No changes made")
1787
- return 0
1788
1233
 
1789
1234
  # Validate syntax
1790
1235
  try: