mcli-framework 7.2.0__py3-none-any.whl → 7.4.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 (97) hide show
  1. mcli/__init__.py +160 -0
  2. mcli/__main__.py +14 -0
  3. mcli/app/__init__.py +23 -0
  4. mcli/app/commands_cmd.py +741 -0
  5. mcli/app/model/__init__.py +0 -0
  6. mcli/app/video/__init__.py +5 -0
  7. mcli/chat/__init__.py +34 -0
  8. mcli/lib/__init__.py +0 -0
  9. mcli/lib/api/__init__.py +0 -0
  10. mcli/lib/auth/__init__.py +1 -0
  11. mcli/lib/config/__init__.py +1 -0
  12. mcli/lib/erd/__init__.py +25 -0
  13. mcli/lib/files/__init__.py +0 -0
  14. mcli/lib/fs/__init__.py +1 -0
  15. mcli/lib/logger/__init__.py +3 -0
  16. mcli/lib/performance/__init__.py +17 -0
  17. mcli/lib/pickles/__init__.py +1 -0
  18. mcli/lib/shell/__init__.py +0 -0
  19. mcli/lib/toml/__init__.py +1 -0
  20. mcli/lib/watcher/__init__.py +0 -0
  21. mcli/ml/__init__.py +16 -0
  22. mcli/ml/api/__init__.py +30 -0
  23. mcli/ml/api/routers/__init__.py +27 -0
  24. mcli/ml/api/schemas.py +2 -2
  25. mcli/ml/auth/__init__.py +45 -0
  26. mcli/ml/auth/models.py +2 -2
  27. mcli/ml/backtesting/__init__.py +39 -0
  28. mcli/ml/cli/__init__.py +5 -0
  29. mcli/ml/cli/main.py +1 -1
  30. mcli/ml/config/__init__.py +33 -0
  31. mcli/ml/configs/__init__.py +16 -0
  32. mcli/ml/dashboard/__init__.py +12 -0
  33. mcli/ml/dashboard/app_integrated.py +296 -30
  34. mcli/ml/dashboard/app_training.py +1 -1
  35. mcli/ml/dashboard/components/__init__.py +7 -0
  36. mcli/ml/dashboard/pages/__init__.py +6 -0
  37. mcli/ml/dashboard/pages/cicd.py +1 -1
  38. mcli/ml/dashboard/pages/debug_dependencies.py +364 -0
  39. mcli/ml/dashboard/pages/gravity_viz.py +565 -0
  40. mcli/ml/dashboard/pages/monte_carlo_predictions.py +555 -0
  41. mcli/ml/dashboard/pages/overview.py +378 -0
  42. mcli/ml/dashboard/pages/predictions_enhanced.py +20 -6
  43. mcli/ml/dashboard/pages/scrapers_and_logs.py +22 -6
  44. mcli/ml/dashboard/pages/test_portfolio.py +423 -0
  45. mcli/ml/dashboard/pages/trading.py +768 -0
  46. mcli/ml/dashboard/streamlit_extras_utils.py +297 -0
  47. mcli/ml/dashboard/utils.py +161 -0
  48. mcli/ml/dashboard/warning_suppression.py +34 -0
  49. mcli/ml/data_ingestion/__init__.py +39 -0
  50. mcli/ml/database/__init__.py +47 -0
  51. mcli/ml/database/session.py +169 -16
  52. mcli/ml/experimentation/__init__.py +29 -0
  53. mcli/ml/features/__init__.py +39 -0
  54. mcli/ml/mlops/__init__.py +33 -0
  55. mcli/ml/models/__init__.py +94 -0
  56. mcli/ml/monitoring/__init__.py +25 -0
  57. mcli/ml/optimization/__init__.py +27 -0
  58. mcli/ml/predictions/__init__.py +5 -0
  59. mcli/ml/predictions/monte_carlo.py +428 -0
  60. mcli/ml/preprocessing/__init__.py +28 -0
  61. mcli/ml/scripts/__init__.py +1 -0
  62. mcli/ml/trading/__init__.py +66 -0
  63. mcli/ml/trading/alpaca_client.py +417 -0
  64. mcli/ml/trading/migrations.py +164 -0
  65. mcli/ml/trading/models.py +418 -0
  66. mcli/ml/trading/paper_trading.py +326 -0
  67. mcli/ml/trading/risk_management.py +370 -0
  68. mcli/ml/trading/trading_service.py +480 -0
  69. mcli/ml/training/__init__.py +10 -0
  70. mcli/mygroup/__init__.py +3 -0
  71. mcli/public/__init__.py +1 -0
  72. mcli/public/commands/__init__.py +2 -0
  73. mcli/self/__init__.py +3 -0
  74. mcli/self/self_cmd.py +514 -15
  75. mcli/workflow/__init__.py +0 -0
  76. mcli/workflow/daemon/__init__.py +15 -0
  77. mcli/workflow/daemon/daemon.py +21 -3
  78. mcli/workflow/dashboard/__init__.py +5 -0
  79. mcli/workflow/docker/__init__.py +0 -0
  80. mcli/workflow/file/__init__.py +0 -0
  81. mcli/workflow/gcloud/__init__.py +1 -0
  82. mcli/workflow/git_commit/__init__.py +0 -0
  83. mcli/workflow/interview/__init__.py +0 -0
  84. mcli/workflow/politician_trading/__init__.py +4 -0
  85. mcli/workflow/registry/__init__.py +0 -0
  86. mcli/workflow/repo/__init__.py +0 -0
  87. mcli/workflow/scheduler/__init__.py +25 -0
  88. mcli/workflow/search/__init__.py +0 -0
  89. mcli/workflow/sync/__init__.py +5 -0
  90. mcli/workflow/videos/__init__.py +1 -0
  91. mcli/workflow/wakatime/__init__.py +80 -0
  92. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/METADATA +4 -1
  93. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/RECORD +97 -18
  94. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/WHEEL +0 -0
  95. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/entry_points.txt +0 -0
  96. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.0.dist-info}/licenses/LICENSE +0 -0
  97. {mcli_framework-7.2.0.dist-info → mcli_framework-7.4.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,23 +417,177 @@ def collect_commands() -> List[Dict[str, Any]]:
387
417
  return commands
388
418
 
389
419
 
390
- @self_app.command("add-command")
420
+ def open_editor_for_command(command_name: str, command_group: str, description: str) -> Optional[str]:
421
+ """
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
431
+ """
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
477
+
478
+ logger = get_logger()
479
+
480
+ # Write your command logic here:
481
+ # Replace this template with your actual command implementation
482
+
483
+ {template.split('"""')[2].split('"""')[0] if '"""' in template else ''}
484
+
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
564
+
565
+
566
+ @self_app.command("add-command", deprecated=True)
391
567
  @click.argument("command_name", required=True)
392
568
  @click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
393
569
  @click.option(
394
570
  "--description", "-d", help="Description for the command", default="Custom command"
395
571
  )
396
- def add_command(command_name, group, description):
572
+ @click.option(
573
+ "--template", "-t", is_flag=True, help="Use template mode (skip editor and use predefined template)"
574
+ )
575
+ def add_command(command_name, group, description, template):
397
576
  """
577
+ [DEPRECATED] Use 'mcli commands add' instead.
578
+
398
579
  Generate a new portable custom command saved to ~/.mcli/commands/.
399
580
 
400
- Commands are automatically nested under the 'workflow' group by default,
401
- making them portable and persistent across updates.
581
+ This command has been moved to 'mcli commands add' for better organization.
582
+ Please use the new command going forward.
402
583
 
403
584
  Example:
404
- mcli self add-command my_command
405
- mcli self add-command analytics --group data
585
+ mcli commands add my_command
586
+ mcli commands add analytics --group data
587
+ mcli commands add quick_cmd --template # Use template without editor
406
588
  """
589
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands add'[/yellow]")
590
+ click.echo("[yellow] Please use: mcli commands add {} [options][/yellow]\n".format(command_name))
407
591
  command_name = command_name.lower().replace("-", "_")
408
592
 
409
593
  # Validate command name
@@ -448,7 +632,17 @@ def add_command(command_name, group, description):
448
632
  return 1
449
633
 
450
634
  # Generate command code
451
- code = get_command_template(command_name, command_group)
635
+ if template:
636
+ # Use template mode - generate and save directly
637
+ code = get_command_template(command_name, command_group)
638
+ click.echo(f"📝 Using template for command: {command_name}")
639
+ else:
640
+ # Editor mode - open editor for user to write code
641
+ click.echo(f"🔧 Opening editor for command: {command_name}")
642
+ code = open_editor_for_command(command_name, command_group, description)
643
+ if code is None:
644
+ click.echo("❌ Command creation cancelled.")
645
+ return 1
452
646
 
453
647
  # Save the command
454
648
  saved_path = manager.save_command(
@@ -469,11 +663,16 @@ def add_command(command_name, group, description):
469
663
  return 0
470
664
 
471
665
 
472
- @self_app.command("list-commands")
666
+ @self_app.command("list-commands", deprecated=True)
473
667
  def list_commands():
474
668
  """
669
+ [DEPRECATED] Use 'mcli commands list-custom' instead.
670
+
475
671
  List all custom commands stored in ~/.mcli/commands/.
476
672
  """
673
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands list-custom'[/yellow]")
674
+ click.echo("[yellow] Please use: mcli commands list-custom[/yellow]\n")
675
+
477
676
  manager = get_command_manager()
478
677
  commands = manager.load_all_commands()
479
678
 
@@ -507,13 +706,18 @@ def list_commands():
507
706
  return 0
508
707
 
509
708
 
510
- @self_app.command("remove-command")
709
+ @self_app.command("remove-command", deprecated=True)
511
710
  @click.argument("command_name", required=True)
512
711
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
513
712
  def remove_command(command_name, yes):
514
713
  """
714
+ [DEPRECATED] Use 'mcli commands remove' instead.
715
+
515
716
  Remove a custom command from ~/.mcli/commands/.
516
717
  """
718
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands remove'[/yellow]")
719
+ click.echo(f"[yellow] Please use: mcli commands remove {command_name}[/yellow]\n")
720
+
517
721
  manager = get_command_manager()
518
722
  command_file = manager.commands_dir / f"{command_name}.json"
519
723
 
@@ -537,14 +741,19 @@ def remove_command(command_name, yes):
537
741
  return 1
538
742
 
539
743
 
540
- @self_app.command("export-commands")
744
+ @self_app.command("export-commands", deprecated=True)
541
745
  @click.argument("export_file", type=click.Path(), required=False)
542
746
  def export_commands(export_file):
543
747
  """
748
+ [DEPRECATED] Use 'mcli commands export' instead.
749
+
544
750
  Export all custom commands to a JSON file.
545
751
 
546
752
  If no file is specified, exports to commands-export.json in current directory.
547
753
  """
754
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands export'[/yellow]")
755
+ click.echo(f"[yellow] Please use: mcli commands export {export_file or '<file>'}[/yellow]\n")
756
+
548
757
  manager = get_command_manager()
549
758
 
550
759
  if not export_file:
@@ -563,13 +772,18 @@ def export_commands(export_file):
563
772
  return 1
564
773
 
565
774
 
566
- @self_app.command("import-commands")
775
+ @self_app.command("import-commands", deprecated=True)
567
776
  @click.argument("import_file", type=click.Path(exists=True), required=True)
568
777
  @click.option("--overwrite", is_flag=True, help="Overwrite existing commands")
569
778
  def import_commands(import_file, overwrite):
570
779
  """
780
+ [DEPRECATED] Use 'mcli commands import' instead.
781
+
571
782
  Import custom commands from a JSON file.
572
783
  """
784
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands import'[/yellow]")
785
+ click.echo(f"[yellow] Please use: mcli commands import {import_file}[/yellow]\n")
786
+
573
787
  manager = get_command_manager()
574
788
  import_path = Path(import_file)
575
789
 
@@ -593,11 +807,16 @@ def import_commands(import_file, overwrite):
593
807
  return 0
594
808
 
595
809
 
596
- @self_app.command("verify-commands")
810
+ @self_app.command("verify-commands", deprecated=True)
597
811
  def verify_commands():
598
812
  """
813
+ [DEPRECATED] Use 'mcli commands verify' instead.
814
+
599
815
  Verify that custom commands match the lockfile.
600
816
  """
817
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands verify'[/yellow]")
818
+ click.echo("[yellow] Please use: mcli commands verify[/yellow]\n")
819
+
601
820
  manager = get_command_manager()
602
821
 
603
822
  # First, ensure lockfile is up to date
@@ -631,11 +850,16 @@ def verify_commands():
631
850
  return 1
632
851
 
633
852
 
634
- @self_app.command("update-lockfile")
853
+ @self_app.command("update-lockfile", deprecated=True)
635
854
  def update_lockfile():
636
855
  """
856
+ [DEPRECATED] Use 'mcli commands update-lockfile' instead.
857
+
637
858
  Update the commands lockfile with current state.
638
859
  """
860
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands update-lockfile'[/yellow]")
861
+ click.echo("[yellow] Please use: mcli commands update-lockfile[/yellow]\n")
862
+
639
863
  manager = get_command_manager()
640
864
 
641
865
  if manager.update_lockfile():
@@ -1557,6 +1781,281 @@ def update(check: bool, pre: bool, yes: bool, skip_ci_check: bool):
1557
1781
  console.print(f"[dim]{traceback.format_exc()}[/dim]")
1558
1782
 
1559
1783
 
1784
+ @self_app.command("import-script", deprecated=True)
1785
+ @click.argument("script_path", type=click.Path(exists=True))
1786
+ @click.option("--name", "-n", help="Command name (defaults to script filename)")
1787
+ @click.option("--group", "-g", default="workflow", help="Command group")
1788
+ @click.option("--description", "-d", help="Command description")
1789
+ @click.option("--interactive", "-i", is_flag=True, help="Open in $EDITOR for review/editing")
1790
+ def import_script(script_path, name, group, description, interactive):
1791
+ """
1792
+ [DEPRECATED] Use 'mcli commands import-script' instead.
1793
+
1794
+ Import a Python script as a portable JSON command.
1795
+
1796
+ Converts a Python script into a JSON command that can be loaded
1797
+ by mcli. The script should define Click commands.
1798
+
1799
+ Examples:
1800
+ mcli commands import-script my_script.py
1801
+ mcli commands import-script my_script.py --name custom-cmd --interactive
1802
+ """
1803
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands import-script'[/yellow]")
1804
+ click.echo(f"[yellow] Please use: mcli commands import-script {script_path}[/yellow]\n")
1805
+
1806
+ import subprocess
1807
+ import tempfile
1808
+
1809
+ script_file = Path(script_path).resolve()
1810
+
1811
+ if not script_file.exists():
1812
+ click.echo(f"❌ Script not found: {script_file}", err=True)
1813
+ return 1
1814
+
1815
+ # Read the script content
1816
+ try:
1817
+ with open(script_file, 'r') as f:
1818
+ code = f.read()
1819
+ except Exception as e:
1820
+ click.echo(f"❌ Failed to read script: {e}", err=True)
1821
+ return 1
1822
+
1823
+ # Determine command name
1824
+ if not name:
1825
+ name = script_file.stem.lower().replace("-", "_")
1826
+
1827
+ # Validate command name
1828
+ if not re.match(r"^[a-z][a-z0-9_]*$", name):
1829
+ click.echo(f"❌ Invalid command name: {name}", err=True)
1830
+ return 1
1831
+
1832
+ # Interactive editing
1833
+ if interactive:
1834
+ editor = os.environ.get('EDITOR', 'vim')
1835
+ click.echo(f"📝 Opening in {editor} for review...")
1836
+
1837
+ # Create temp file with the code
1838
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as tmp:
1839
+ tmp.write(code)
1840
+ tmp_path = tmp.name
1841
+
1842
+ try:
1843
+ subprocess.run([editor, tmp_path], check=True)
1844
+
1845
+ # Read edited content
1846
+ with open(tmp_path, 'r') as f:
1847
+ code = f.read()
1848
+ finally:
1849
+ Path(tmp_path).unlink(missing_ok=True)
1850
+
1851
+ # Get description
1852
+ if not description:
1853
+ # Try to extract from docstring
1854
+ import ast
1855
+ try:
1856
+ tree = ast.parse(code)
1857
+ description = ast.get_docstring(tree) or f"Imported from {script_file.name}"
1858
+ except:
1859
+ description = f"Imported from {script_file.name}"
1860
+
1861
+ # Save as JSON command
1862
+ manager = get_command_manager()
1863
+
1864
+ saved_path = manager.save_command(
1865
+ name=name,
1866
+ code=code,
1867
+ description=description,
1868
+ group=group,
1869
+ metadata={
1870
+ "source": "import-script",
1871
+ "original_file": str(script_file),
1872
+ "imported_at": datetime.now().isoformat()
1873
+ }
1874
+ )
1875
+
1876
+ click.echo(f"✅ Imported script as command: {name}")
1877
+ click.echo(f"📁 Saved to: {saved_path}")
1878
+ click.echo(f"🔄 Use with: mcli {group} {name}")
1879
+ click.echo(f"💡 Command will be available after restart or reload")
1880
+
1881
+ return 0
1882
+
1883
+
1884
+ @self_app.command("export-script", deprecated=True)
1885
+ @click.argument("command_name")
1886
+ @click.option("--output", "-o", type=click.Path(), help="Output file path")
1887
+ @click.option("--standalone", "-s", is_flag=True, help="Make script standalone (add if __name__ == '__main__')")
1888
+ def export_script(command_name, output, standalone):
1889
+ """
1890
+ [DEPRECATED] Use 'mcli commands export-script' instead.
1891
+
1892
+ Export a JSON command to a Python script.
1893
+
1894
+ Converts a portable JSON command back to a standalone Python script
1895
+ that can be edited and run independently.
1896
+
1897
+ Examples:
1898
+ mcli commands export-script my-command
1899
+ mcli commands export-script my-command --output my_script.py --standalone
1900
+ """
1901
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands export-script'[/yellow]")
1902
+ click.echo(f"[yellow] Please use: mcli commands export-script {command_name}[/yellow]\n")
1903
+
1904
+ manager = get_command_manager()
1905
+
1906
+ # Load the command
1907
+ command_file = manager.commands_dir / f"{command_name}.json"
1908
+ if not command_file.exists():
1909
+ click.echo(f"❌ Command not found: {command_name}", err=True)
1910
+ return 1
1911
+
1912
+ try:
1913
+ with open(command_file, 'r') as f:
1914
+ command_data = json.load(f)
1915
+ except Exception as e:
1916
+ click.echo(f"❌ Failed to load command: {e}", err=True)
1917
+ return 1
1918
+
1919
+ # Get the code
1920
+ code = command_data.get('code', '')
1921
+
1922
+ if not code:
1923
+ click.echo(f"❌ Command has no code: {command_name}", err=True)
1924
+ return 1
1925
+
1926
+ # Add standalone wrapper if requested
1927
+ if standalone:
1928
+ # Check if already has if __name__ == '__main__'
1929
+ if "if __name__" not in code:
1930
+ code += "\n\nif __name__ == '__main__':\n app()\n"
1931
+
1932
+ # Determine output path
1933
+ if not output:
1934
+ output = f"{command_name}.py"
1935
+
1936
+ output_file = Path(output)
1937
+
1938
+ # Write the script
1939
+ try:
1940
+ with open(output_file, 'w') as f:
1941
+ f.write(code)
1942
+ except Exception as e:
1943
+ click.echo(f"❌ Failed to write script: {e}", err=True)
1944
+ return 1
1945
+
1946
+ click.echo(f"✅ Exported command to script: {output_file}")
1947
+ click.echo(f"📝 Source command: {command_name}")
1948
+
1949
+ if standalone:
1950
+ click.echo(f"🚀 Run standalone with: python {output_file}")
1951
+
1952
+ click.echo(f"💡 Edit and re-import with: mcli self import-script {output_file}")
1953
+
1954
+ return 0
1955
+
1956
+
1957
+ @self_app.command("edit-command", deprecated=True)
1958
+ @click.argument("command_name")
1959
+ @click.option("--editor", "-e", help="Editor to use (defaults to $EDITOR)")
1960
+ def edit_command(command_name, editor):
1961
+ """
1962
+ [DEPRECATED] Use 'mcli commands edit' instead.
1963
+
1964
+ Edit a command interactively using $EDITOR.
1965
+
1966
+ Opens the command's Python code in your preferred editor,
1967
+ allows you to make changes, and saves the updated version.
1968
+
1969
+ Examples:
1970
+ mcli commands edit my-command
1971
+ mcli commands edit my-command --editor code
1972
+ """
1973
+ click.echo("[yellow]⚠️ DEPRECATED: This command has been moved to 'mcli commands edit'[/yellow]")
1974
+ click.echo(f"[yellow] Please use: mcli commands edit {command_name}[/yellow]\n")
1975
+
1976
+ import subprocess
1977
+ import tempfile
1978
+
1979
+ manager = get_command_manager()
1980
+
1981
+ # Load the command
1982
+ command_file = manager.commands_dir / f"{command_name}.json"
1983
+ if not command_file.exists():
1984
+ click.echo(f"❌ Command not found: {command_name}", err=True)
1985
+ return 1
1986
+
1987
+ try:
1988
+ with open(command_file, 'r') as f:
1989
+ command_data = json.load(f)
1990
+ except Exception as e:
1991
+ click.echo(f"❌ Failed to load command: {e}", err=True)
1992
+ return 1
1993
+
1994
+ code = command_data.get('code', '')
1995
+
1996
+ if not code:
1997
+ click.echo(f"❌ Command has no code: {command_name}", err=True)
1998
+ return 1
1999
+
2000
+ # Determine editor
2001
+ if not editor:
2002
+ editor = os.environ.get('EDITOR', 'vim')
2003
+
2004
+ click.echo(f"📝 Opening command in {editor}...")
2005
+
2006
+ # Create temp file with the code
2007
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False,
2008
+ prefix=f"{command_name}_") as tmp:
2009
+ tmp.write(code)
2010
+ tmp_path = tmp.name
2011
+
2012
+ try:
2013
+ # Open in editor
2014
+ result = subprocess.run([editor, tmp_path])
2015
+
2016
+ if result.returncode != 0:
2017
+ click.echo(f"⚠️ Editor exited with code {result.returncode}")
2018
+
2019
+ # Read edited content
2020
+ with open(tmp_path, 'r') as f:
2021
+ new_code = f.read()
2022
+
2023
+ # Check if code changed
2024
+ if new_code.strip() == code.strip():
2025
+ click.echo("ℹ️ No changes made")
2026
+ return 0
2027
+
2028
+ # Validate syntax
2029
+ try:
2030
+ compile(new_code, '<string>', 'exec')
2031
+ except SyntaxError as e:
2032
+ click.echo(f"❌ Syntax error in edited code: {e}", err=True)
2033
+ should_save = Prompt.ask(
2034
+ "Save anyway?", choices=["y", "n"], default="n"
2035
+ )
2036
+ if should_save.lower() != "y":
2037
+ return 1
2038
+
2039
+ # Update the command
2040
+ command_data['code'] = new_code
2041
+ command_data['updated_at'] = datetime.now().isoformat()
2042
+
2043
+ with open(command_file, 'w') as f:
2044
+ json.dump(command_data, f, indent=2)
2045
+
2046
+ # Update lockfile
2047
+ manager.generate_lockfile()
2048
+
2049
+ click.echo(f"✅ Updated command: {command_name}")
2050
+ click.echo(f"📁 Saved to: {command_file}")
2051
+ click.echo(f"🔄 Reload with: mcli self reload" or "restart mcli")
2052
+
2053
+ finally:
2054
+ Path(tmp_path).unlink(missing_ok=True)
2055
+
2056
+ return 0
2057
+
2058
+
1560
2059
  # Register the plugin group with self_app
1561
2060
  self_app.add_command(plugin)
1562
2061
 
File without changes
@@ -0,0 +1,15 @@
1
+ """
2
+ Daemon service for command management and execution.
3
+
4
+ This module provides a background daemon service that can store, manage, and execute
5
+ commands written in various programming languages (Python, Node.js, Lua, Shell).
6
+ Commands are stored in a SQLite database with embeddings for similarity search and
7
+ hierarchical grouping.
8
+
9
+ The daemon CLI commands are now loaded from portable JSON files in ~/.mcli/commands/
10
+ """
11
+
12
+ from .daemon import Command, CommandExecutor, DaemonService
13
+
14
+ # Export main components
15
+ __all__ = ["Command", "CommandExecutor", "DaemonService"]
@@ -16,17 +16,35 @@ import click
16
16
  import psutil
17
17
  from sklearn.feature_extraction.text import TfidfVectorizer
18
18
  from sklearn.metrics.pairwise import cosine_similarity
19
- from watchdog.events import FileSystemEventHandler
20
- from watchdog.observers import Observer
19
+
20
+ try:
21
+ from watchdog.events import FileSystemEventHandler
22
+ from watchdog.observers import Observer
23
+ HAS_WATCHDOG = True
24
+ except ImportError:
25
+ # Watchdog not available, file watching will be disabled
26
+ HAS_WATCHDOG = False
27
+ FileSystemEventHandler = object # Stub for inheritance
28
+ Observer = None
21
29
 
22
30
  # Import existing utilities
23
31
  from mcli.lib.logger.logger import get_logger
24
32
  from mcli.lib.toml.toml import read_from_toml
25
- from mcli.workflow.daemon.commands import CommandDatabase
26
33
 
27
34
  logger = get_logger(__name__)
28
35
 
29
36
 
37
+ # Stub CommandDatabase for backward compatibility
38
+ # Commands are now managed via JSON files in ~/.mcli/commands/
39
+ class CommandDatabase:
40
+ """Stub database for backward compatibility.
41
+ Commands are now stored as JSON files and loaded via the custom commands system.
42
+ """
43
+ def __init__(self, db_path: Optional[str] = None):
44
+ logger.debug("CommandDatabase stub initialized - commands now managed via JSON files")
45
+ pass
46
+
47
+
30
48
  @dataclass
31
49
  class Command:
32
50
  """Represents a stored command"""