mcli-framework 7.1.3__py3-none-any.whl → 7.3.1__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 (114) hide show
  1. mcli/__init__.py +160 -0
  2. mcli/__main__.py +14 -0
  3. mcli/app/__init__.py +23 -0
  4. mcli/app/main.py +10 -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/custom_commands.py +424 -0
  13. mcli/lib/erd/__init__.py +25 -0
  14. mcli/lib/files/__init__.py +0 -0
  15. mcli/lib/fs/__init__.py +1 -0
  16. mcli/lib/logger/__init__.py +3 -0
  17. mcli/lib/paths.py +12 -0
  18. mcli/lib/performance/__init__.py +17 -0
  19. mcli/lib/pickles/__init__.py +1 -0
  20. mcli/lib/shell/__init__.py +0 -0
  21. mcli/lib/toml/__init__.py +1 -0
  22. mcli/lib/watcher/__init__.py +0 -0
  23. mcli/ml/__init__.py +16 -0
  24. mcli/ml/api/__init__.py +30 -0
  25. mcli/ml/api/routers/__init__.py +27 -0
  26. mcli/ml/api/schemas.py +2 -2
  27. mcli/ml/auth/__init__.py +45 -0
  28. mcli/ml/auth/models.py +2 -2
  29. mcli/ml/backtesting/__init__.py +39 -0
  30. mcli/ml/cli/__init__.py +5 -0
  31. mcli/ml/cli/main.py +1 -1
  32. mcli/ml/config/__init__.py +33 -0
  33. mcli/ml/configs/__init__.py +16 -0
  34. mcli/ml/dashboard/__init__.py +12 -0
  35. mcli/ml/dashboard/app.py +13 -13
  36. mcli/ml/dashboard/app_integrated.py +1309 -148
  37. mcli/ml/dashboard/app_supabase.py +46 -21
  38. mcli/ml/dashboard/app_training.py +14 -14
  39. mcli/ml/dashboard/components/__init__.py +7 -0
  40. mcli/ml/dashboard/components/charts.py +258 -0
  41. mcli/ml/dashboard/components/metrics.py +125 -0
  42. mcli/ml/dashboard/components/tables.py +228 -0
  43. mcli/ml/dashboard/pages/__init__.py +6 -0
  44. mcli/ml/dashboard/pages/cicd.py +382 -0
  45. mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
  46. mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
  47. mcli/ml/dashboard/pages/test_portfolio.py +373 -0
  48. mcli/ml/dashboard/pages/trading.py +714 -0
  49. mcli/ml/dashboard/pages/workflows.py +533 -0
  50. mcli/ml/dashboard/utils.py +154 -0
  51. mcli/ml/data_ingestion/__init__.py +39 -0
  52. mcli/ml/database/__init__.py +47 -0
  53. mcli/ml/experimentation/__init__.py +29 -0
  54. mcli/ml/features/__init__.py +39 -0
  55. mcli/ml/mlops/__init__.py +33 -0
  56. mcli/ml/models/__init__.py +94 -0
  57. mcli/ml/monitoring/__init__.py +25 -0
  58. mcli/ml/optimization/__init__.py +27 -0
  59. mcli/ml/predictions/__init__.py +5 -0
  60. mcli/ml/preprocessing/__init__.py +28 -0
  61. mcli/ml/scripts/__init__.py +1 -0
  62. mcli/ml/trading/__init__.py +60 -0
  63. mcli/ml/trading/alpaca_client.py +353 -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/ml/training/train_model.py +569 -0
  71. mcli/mygroup/__init__.py +3 -0
  72. mcli/public/__init__.py +1 -0
  73. mcli/public/commands/__init__.py +2 -0
  74. mcli/self/__init__.py +3 -0
  75. mcli/self/self_cmd.py +579 -91
  76. mcli/workflow/__init__.py +0 -0
  77. mcli/workflow/daemon/__init__.py +15 -0
  78. mcli/workflow/daemon/daemon.py +21 -3
  79. mcli/workflow/dashboard/__init__.py +5 -0
  80. mcli/workflow/docker/__init__.py +0 -0
  81. mcli/workflow/file/__init__.py +0 -0
  82. mcli/workflow/gcloud/__init__.py +1 -0
  83. mcli/workflow/git_commit/__init__.py +0 -0
  84. mcli/workflow/interview/__init__.py +0 -0
  85. mcli/workflow/politician_trading/__init__.py +4 -0
  86. mcli/workflow/politician_trading/data_sources.py +259 -1
  87. mcli/workflow/politician_trading/models.py +159 -1
  88. mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
  89. mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
  90. mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
  91. mcli/workflow/politician_trading/seed_database.py +539 -0
  92. mcli/workflow/registry/__init__.py +0 -0
  93. mcli/workflow/repo/__init__.py +0 -0
  94. mcli/workflow/scheduler/__init__.py +25 -0
  95. mcli/workflow/search/__init__.py +0 -0
  96. mcli/workflow/sync/__init__.py +5 -0
  97. mcli/workflow/videos/__init__.py +1 -0
  98. mcli/workflow/wakatime/__init__.py +80 -0
  99. mcli/workflow/workflow.py +8 -27
  100. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
  101. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
  102. mcli/workflow/daemon/api_daemon.py +0 -800
  103. mcli/workflow/daemon/commands.py +0 -1196
  104. mcli/workflow/dashboard/dashboard_cmd.py +0 -120
  105. mcli/workflow/file/file.py +0 -100
  106. mcli/workflow/git_commit/commands.py +0 -430
  107. mcli/workflow/politician_trading/commands.py +0 -1939
  108. mcli/workflow/scheduler/commands.py +0 -493
  109. mcli/workflow/sync/sync_cmd.py +0 -437
  110. mcli/workflow/videos/videos.py +0 -242
  111. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
  112. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
  113. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
  114. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
mcli/__init__.py ADDED
@@ -0,0 +1,160 @@
1
+ # logger.info("I am in mcli.__init__.py")
2
+ import sys
3
+
4
+ try:
5
+ from mcli.app import main
6
+
7
+ # from mcli.public import *
8
+ # from mcli.private import *
9
+ # Import the complete Click superset decorators
10
+ from mcli.lib.api.mcli_decorators import BOOL # mcli.BOOL - Click BOOL type
11
+ from mcli.lib.api.mcli_decorators import FLOAT # mcli.FLOAT - Click FLOAT type
12
+ from mcli.lib.api.mcli_decorators import INT # mcli.INT - Click INT type
13
+ from mcli.lib.api.mcli_decorators import STRING # mcli.STRING - Click STRING type
14
+ from mcli.lib.api.mcli_decorators import UNPROCESSED # mcli.UNPROCESSED - Click UNPROCESSED
15
+ from mcli.lib.api.mcli_decorators import UUID # mcli.UUID - Click UUID type
16
+ from mcli.lib.api.mcli_decorators import Abort # mcli.Abort - Click Abort
17
+ from mcli.lib.api.mcli_decorators import BadParameter # mcli.BadParameter - Click BadParameter
18
+ from mcli.lib.api.mcli_decorators import Choice # mcli.Choice - Click Choice type
19
+ from mcli.lib.api.mcli_decorators import File # mcli.File - Click File type
20
+ from mcli.lib.api.mcli_decorators import FloatRange # mcli.FloatRange - Click FloatRange type
21
+ from mcli.lib.api.mcli_decorators import IntRange # mcli.IntRange - Click IntRange type
22
+ from mcli.lib.api.mcli_decorators import ParamType # mcli.ParamType - Click ParamType
23
+ from mcli.lib.api.mcli_decorators import Path # mcli.Path - Click Path type
24
+ from mcli.lib.api.mcli_decorators import UsageError # mcli.UsageError - Click UsageError
25
+ from mcli.lib.api.mcli_decorators import api # @mcli.api - Legacy API decorator
26
+ from mcli.lib.api.mcli_decorators import (
27
+ api_command, # @mcli.api_command - Convenience for API endpoints
28
+ )
29
+ from mcli.lib.api.mcli_decorators import argument # @mcli.argument - Click argument decorator
30
+ from mcli.lib.api.mcli_decorators import (
31
+ background, # @mcli.background - Legacy background decorator
32
+ )
33
+ from mcli.lib.api.mcli_decorators import (
34
+ background_command, # @mcli.background_command - Convenience for background
35
+ )
36
+ from mcli.lib.api.mcli_decorators import clear # mcli.clear - Click clear
37
+ from mcli.lib.api.mcli_decorators import (
38
+ cli_with_api, # @mcli.cli_with_api - Legacy combined decorator
39
+ )
40
+ from mcli.lib.api.mcli_decorators import (
41
+ command, # @mcli.command - Complete Click command with API/background
42
+ )
43
+ from mcli.lib.api.mcli_decorators import confirm # mcli.confirm - Click confirmation
44
+ from mcli.lib.api.mcli_decorators import echo # mcli.echo - Click echo function
45
+ from mcli.lib.api.mcli_decorators import edit # mcli.edit - Click editor
46
+ from mcli.lib.api.mcli_decorators import (
47
+ format_filename, # mcli.format_filename - Click filename
48
+ )
49
+ from mcli.lib.api.mcli_decorators import get_app # mcli.get_app - Click app
50
+ from mcli.lib.api.mcli_decorators import get_app_dir # mcli.get_app_dir - Click app directory
51
+ from mcli.lib.api.mcli_decorators import (
52
+ get_binary_stream, # mcli.get_binary_stream - Click binary stream
53
+ )
54
+ from mcli.lib.api.mcli_decorators import (
55
+ get_current_context, # mcli.get_current_context - Click context
56
+ )
57
+ from mcli.lib.api.mcli_decorators import (
58
+ get_network_credentials, # mcli.get_network_credentials - Click network
59
+ )
60
+ from mcli.lib.api.mcli_decorators import get_os_args # mcli.get_os_args - Click OS args
61
+ from mcli.lib.api.mcli_decorators import (
62
+ get_terminal_size, # mcli.get_terminal_size - Click terminal size
63
+ )
64
+ from mcli.lib.api.mcli_decorators import (
65
+ get_text_stream, # mcli.get_text_stream - Click text stream
66
+ )
67
+ from mcli.lib.api.mcli_decorators import getchar # mcli.getchar - Click character input
68
+ from mcli.lib.api.mcli_decorators import (
69
+ group, # @mcli.group - Complete Click group with API support
70
+ )
71
+ from mcli.lib.api.mcli_decorators import launch # mcli.launch - Click launch
72
+ from mcli.lib.api.mcli_decorators import open_file # mcli.open_file - Click file operations
73
+ from mcli.lib.api.mcli_decorators import option # @mcli.option - Click option decorator
74
+ from mcli.lib.api.mcli_decorators import pause # mcli.pause - Click pause
75
+ from mcli.lib.api.mcli_decorators import progressbar # mcli.progressbar - Click progress bar
76
+ from mcli.lib.api.mcli_decorators import prompt # mcli.prompt - Click prompt
77
+ from mcli.lib.api.mcli_decorators import secho # mcli.secho - Click styled echo
78
+ from mcli.lib.api.mcli_decorators import style # mcli.style - Click styling
79
+ from mcli.lib.api.mcli_decorators import unstyle # mcli.unstyle - Click unstyle
80
+ from mcli.lib.api.mcli_decorators import ( # Core decorators (complete Click superset); Click re-exports (complete subsume); Click types (complete subsume); Convenience decorators; Legacy decorators (for backward compatibility); Server management; Configuration; Convenience functions
81
+ disable_api_server,
82
+ enable_api_server,
83
+ get_api_config,
84
+ health_check,
85
+ is_background_available,
86
+ is_server_running,
87
+ start_server,
88
+ status_check,
89
+ stop_server,
90
+ )
91
+
92
+ # Make everything available at the top level (complete Click subsume)
93
+ __all__ = [
94
+ "main",
95
+ # Core decorators (complete Click superset)
96
+ "command",
97
+ "group",
98
+ # Click re-exports (complete subsume)
99
+ "option",
100
+ "argument",
101
+ "echo",
102
+ "get_current_context",
103
+ "get_app",
104
+ "launch",
105
+ "open_file",
106
+ "get_os_args",
107
+ "get_binary_stream",
108
+ "get_text_stream",
109
+ "format_filename",
110
+ "getchar",
111
+ "pause",
112
+ "clear",
113
+ "style",
114
+ "unstyle",
115
+ "secho",
116
+ "edit",
117
+ "confirm",
118
+ "prompt",
119
+ "progressbar",
120
+ "get_terminal_size",
121
+ "get_app_dir",
122
+ "get_network_credentials",
123
+ # Click types (complete subsume)
124
+ "Path",
125
+ "Choice",
126
+ "IntRange",
127
+ "FloatRange",
128
+ "UNPROCESSED",
129
+ "STRING",
130
+ "INT",
131
+ "FLOAT",
132
+ "BOOL",
133
+ "UUID",
134
+ "File",
135
+ "ParamType",
136
+ "BadParameter",
137
+ "UsageError",
138
+ "Abort",
139
+ # Convenience decorators
140
+ "api_command",
141
+ "background_command",
142
+ # Legacy decorators (for backward compatibility)
143
+ "api",
144
+ "background",
145
+ "cli_with_api",
146
+ # Server management
147
+ "start_server",
148
+ "stop_server",
149
+ "is_server_running",
150
+ "is_background_available",
151
+ "get_api_config",
152
+ "enable_api_server",
153
+ "disable_api_server",
154
+ "health_check",
155
+ "status_check",
156
+ ]
157
+
158
+ except ImportError:
159
+ # from .app import main
160
+ sys.exit(1)
mcli/__main__.py ADDED
@@ -0,0 +1,14 @@
1
+ # logger.info("I am in mcli.__init__.py")
2
+
3
+ try:
4
+ from mcli.app.main import main as main_func
5
+
6
+ # from mcli.public import *
7
+ # from mcli.private import *
8
+ except ImportError:
9
+ from .app.main import main as main_func
10
+
11
+ __all__ = ["main_func"]
12
+
13
+ if __name__ == "__main__":
14
+ main_func()
mcli/app/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ # src/mcli/app/__init__.py
2
+ import importlib
3
+ from pathlib import Path
4
+
5
+ from mcli.lib.logger.logger import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+ logger.info("Initializing mcli.app package")
10
+
11
+ # Import main function
12
+ try:
13
+ from .main import main
14
+
15
+ logger.info("Successfully imported main from .main")
16
+ except ImportError as e:
17
+ logger.error(f"Failed to import main: {e}")
18
+ import traceback
19
+
20
+ logger.error(f"Traceback: {traceback.format_exc()}")
21
+ raise
22
+
23
+ __all__ = ["main"]
mcli/app/main.py CHANGED
@@ -423,6 +423,16 @@ def _add_lazy_commands(app: click.Group):
423
423
  app.add_command(lazy_cmd)
424
424
  logger.debug(f"Added lazy command: {cmd_name}")
425
425
 
426
+ # Load custom user commands from ~/.mcli/commands/ AFTER all groups are added
427
+ try:
428
+ from mcli.lib.custom_commands import load_custom_commands
429
+
430
+ loaded_count = load_custom_commands(app)
431
+ if loaded_count > 0:
432
+ logger.info(f"Loaded {loaded_count} custom user command(s)")
433
+ except Exception as e:
434
+ logger.debug(f"Could not load custom commands: {e}")
435
+
426
436
 
427
437
  def create_app() -> click.Group:
428
438
  """Create and configure the Click application with clean top-level commands."""
File without changes
@@ -0,0 +1,5 @@
1
+ """
2
+ Video processing commands for mcli.
3
+ """
4
+
5
+ from .video import *
mcli/chat/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ MCLI Chat System
3
+ Real-time system control and interaction capabilities for MCLI chat
4
+ """
5
+
6
+ from .system_controller import (
7
+ SystemController,
8
+ control_app,
9
+ execute_system_command,
10
+ open_file_or_url,
11
+ open_textedit_and_write,
12
+ system_controller,
13
+ take_screenshot,
14
+ )
15
+ from .system_integration import (
16
+ ChatSystemIntegration,
17
+ chat_system_integration,
18
+ get_system_capabilities,
19
+ handle_system_request,
20
+ )
21
+
22
+ __all__ = [
23
+ "SystemController",
24
+ "system_controller",
25
+ "ChatSystemIntegration",
26
+ "chat_system_integration",
27
+ "handle_system_request",
28
+ "get_system_capabilities",
29
+ "open_textedit_and_write",
30
+ "control_app",
31
+ "execute_system_command",
32
+ "take_screenshot",
33
+ "open_file_or_url",
34
+ ]
mcli/lib/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1 @@
1
+ from .auth import *
@@ -0,0 +1 @@
1
+ from .config import *
@@ -0,0 +1,424 @@
1
+ """
2
+ Custom command storage and loading for mcli.
3
+
4
+ This module provides functionality to store user-created commands in a portable
5
+ format in ~/.mcli/commands/ and automatically load them at startup.
6
+ """
7
+
8
+ import json
9
+ import importlib.util
10
+ import sys
11
+ import tempfile
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ import click
17
+
18
+ from mcli.lib.logger.logger import get_logger
19
+ from mcli.lib.paths import get_custom_commands_dir
20
+
21
+ logger = get_logger()
22
+
23
+
24
+ class CustomCommandManager:
25
+ """Manages custom user commands stored in JSON format."""
26
+
27
+ def __init__(self):
28
+ self.commands_dir = get_custom_commands_dir()
29
+ self.loaded_commands: Dict[str, Any] = {}
30
+ self.lockfile_path = self.commands_dir / "commands.lock.json"
31
+
32
+ def save_command(
33
+ self,
34
+ name: str,
35
+ code: str,
36
+ description: str = "",
37
+ group: Optional[str] = None,
38
+ metadata: Optional[Dict[str, Any]] = None,
39
+ ) -> Path:
40
+ """
41
+ Save a custom command to the commands directory.
42
+
43
+ Args:
44
+ name: Command name
45
+ code: Python code for the command
46
+ description: Command description
47
+ group: Optional command group
48
+ metadata: Additional metadata
49
+
50
+ Returns:
51
+ Path to the saved command file
52
+ """
53
+ command_data = {
54
+ "name": name,
55
+ "code": code,
56
+ "description": description,
57
+ "group": group,
58
+ "created_at": datetime.utcnow().isoformat() + "Z",
59
+ "updated_at": datetime.utcnow().isoformat() + "Z",
60
+ "version": "1.0",
61
+ "metadata": metadata or {},
62
+ }
63
+
64
+ # Save as JSON file
65
+ command_file = self.commands_dir / f"{name}.json"
66
+ with open(command_file, "w") as f:
67
+ json.dump(command_data, f, indent=2)
68
+
69
+ logger.info(f"Saved custom command: {name} to {command_file}")
70
+
71
+ # Update lockfile
72
+ self.update_lockfile()
73
+
74
+ return command_file
75
+
76
+ def load_command(self, command_file: Path) -> Optional[Dict[str, Any]]:
77
+ """
78
+ Load a command from a JSON file.
79
+
80
+ Args:
81
+ command_file: Path to the command JSON file
82
+
83
+ Returns:
84
+ Command data dictionary or None if loading failed
85
+ """
86
+ try:
87
+ with open(command_file, "r") as f:
88
+ command_data = json.load(f)
89
+ return command_data
90
+ except Exception as e:
91
+ logger.error(f"Failed to load command from {command_file}: {e}")
92
+ return None
93
+
94
+ def load_all_commands(self) -> List[Dict[str, Any]]:
95
+ """
96
+ Load all custom commands from the commands directory.
97
+
98
+ Returns:
99
+ List of command data dictionaries
100
+ """
101
+ commands = []
102
+ for command_file in self.commands_dir.glob("*.json"):
103
+ # Skip the lockfile
104
+ if command_file.name == "commands.lock.json":
105
+ continue
106
+
107
+ command_data = self.load_command(command_file)
108
+ if command_data:
109
+ commands.append(command_data)
110
+ return commands
111
+
112
+ def delete_command(self, name: str) -> bool:
113
+ """
114
+ Delete a custom command.
115
+
116
+ Args:
117
+ name: Command name
118
+
119
+ Returns:
120
+ True if deleted successfully, False otherwise
121
+ """
122
+ command_file = self.commands_dir / f"{name}.json"
123
+ if command_file.exists():
124
+ command_file.unlink()
125
+ logger.info(f"Deleted custom command: {name}")
126
+ self.update_lockfile() # Update lockfile after deletion
127
+ return True
128
+ return False
129
+
130
+ def generate_lockfile(self) -> Dict[str, Any]:
131
+ """
132
+ Generate a lockfile containing metadata about all custom commands.
133
+
134
+ Returns:
135
+ Dictionary containing lockfile data
136
+ """
137
+ commands = self.load_all_commands()
138
+
139
+ lockfile_data = {
140
+ "version": "1.0",
141
+ "generated_at": datetime.utcnow().isoformat() + "Z",
142
+ "commands": {},
143
+ }
144
+
145
+ for command_data in commands:
146
+ name = command_data["name"]
147
+ lockfile_data["commands"][name] = {
148
+ "name": name,
149
+ "description": command_data.get("description", ""),
150
+ "group": command_data.get("group"),
151
+ "version": command_data.get("version", "1.0"),
152
+ "created_at": command_data.get("created_at", ""),
153
+ "updated_at": command_data.get("updated_at", ""),
154
+ }
155
+
156
+ return lockfile_data
157
+
158
+ def update_lockfile(self) -> bool:
159
+ """
160
+ Update the lockfile with current command state.
161
+
162
+ Returns:
163
+ True if successful, False otherwise
164
+ """
165
+ try:
166
+ lockfile_data = self.generate_lockfile()
167
+ with open(self.lockfile_path, "w") as f:
168
+ json.dump(lockfile_data, f, indent=2)
169
+ logger.debug(f"Updated lockfile: {self.lockfile_path}")
170
+ return True
171
+ except Exception as e:
172
+ logger.error(f"Failed to update lockfile: {e}")
173
+ return False
174
+
175
+ def load_lockfile(self) -> Optional[Dict[str, Any]]:
176
+ """
177
+ Load the lockfile.
178
+
179
+ Returns:
180
+ Lockfile data dictionary or None if not found
181
+ """
182
+ if not self.lockfile_path.exists():
183
+ return None
184
+
185
+ try:
186
+ with open(self.lockfile_path, "r") as f:
187
+ return json.load(f)
188
+ except Exception as e:
189
+ logger.error(f"Failed to load lockfile: {e}")
190
+ return None
191
+
192
+ def verify_lockfile(self) -> Dict[str, Any]:
193
+ """
194
+ Verify that the current command state matches the lockfile.
195
+
196
+ Returns:
197
+ Dictionary with verification results:
198
+ - 'valid': bool indicating if lockfile is valid
199
+ - 'missing': list of commands in lockfile but not in filesystem
200
+ - 'extra': list of commands in filesystem but not in lockfile
201
+ - 'modified': list of commands with different metadata
202
+ """
203
+ result = {
204
+ "valid": True,
205
+ "missing": [],
206
+ "extra": [],
207
+ "modified": [],
208
+ }
209
+
210
+ lockfile_data = self.load_lockfile()
211
+ if not lockfile_data:
212
+ result["valid"] = False
213
+ return result
214
+
215
+ current_commands = {cmd["name"]: cmd for cmd in self.load_all_commands()}
216
+ lockfile_commands = lockfile_data.get("commands", {})
217
+
218
+ # Check for missing commands (in lockfile but not in filesystem)
219
+ for name in lockfile_commands:
220
+ if name not in current_commands:
221
+ result["missing"].append(name)
222
+ result["valid"] = False
223
+
224
+ # Check for extra commands (in filesystem but not in lockfile)
225
+ for name in current_commands:
226
+ if name not in lockfile_commands:
227
+ result["extra"].append(name)
228
+ result["valid"] = False
229
+
230
+ # Check for modified commands (different metadata)
231
+ for name in set(current_commands.keys()) & set(lockfile_commands.keys()):
232
+ current = current_commands[name]
233
+ locked = lockfile_commands[name]
234
+
235
+ if current.get("updated_at") != locked.get("updated_at"):
236
+ result["modified"].append(name)
237
+ result["valid"] = False
238
+
239
+ return result
240
+
241
+ def register_command_with_click(
242
+ self, command_data: Dict[str, Any], target_group: click.Group
243
+ ) -> bool:
244
+ """
245
+ Dynamically register a custom command with a Click group.
246
+
247
+ Args:
248
+ command_data: Command data dictionary
249
+ target_group: Click group to register the command with
250
+
251
+ Returns:
252
+ True if successful, False otherwise
253
+ """
254
+ try:
255
+ name = command_data["name"]
256
+ code = command_data["code"]
257
+
258
+ # Create a temporary module to execute the command code
259
+ module_name = f"mcli_custom_{name}"
260
+
261
+ # Create a temporary file to store the code
262
+ with tempfile.NamedTemporaryFile(
263
+ mode="w", suffix=".py", delete=False
264
+ ) as temp_file:
265
+ temp_file.write(code)
266
+ temp_file_path = temp_file.name
267
+
268
+ try:
269
+ # Load the module from the temporary file
270
+ spec = importlib.util.spec_from_file_location(module_name, temp_file_path)
271
+ if spec and spec.loader:
272
+ module = importlib.util.module_from_spec(spec)
273
+ sys.modules[module_name] = module
274
+ spec.loader.exec_module(module)
275
+
276
+ # Look for a command or command group in the module
277
+ command_obj = None
278
+ for attr_name in dir(module):
279
+ attr = getattr(module, attr_name)
280
+ if isinstance(attr, (click.Command, click.Group)):
281
+ command_obj = attr
282
+ break
283
+
284
+ if command_obj:
285
+ # Register with the target group
286
+ target_group.add_command(command_obj, name=name)
287
+ self.loaded_commands[name] = command_obj
288
+ logger.info(f"Registered custom command: {name}")
289
+ return True
290
+ else:
291
+ logger.warning(
292
+ f"No Click command found in custom command: {name}"
293
+ )
294
+ return False
295
+ finally:
296
+ # Clean up temporary file
297
+ Path(temp_file_path).unlink(missing_ok=True)
298
+
299
+ except Exception as e:
300
+ logger.error(f"Failed to register custom command {name}: {e}")
301
+ return False
302
+
303
+ def export_commands(self, export_path: Path) -> bool:
304
+ """
305
+ Export all custom commands to a single JSON file.
306
+
307
+ Args:
308
+ export_path: Path to export file
309
+
310
+ Returns:
311
+ True if successful, False otherwise
312
+ """
313
+ try:
314
+ commands = self.load_all_commands()
315
+ with open(export_path, "w") as f:
316
+ json.dump(commands, f, indent=2)
317
+ logger.info(f"Exported {len(commands)} commands to {export_path}")
318
+ return True
319
+ except Exception as e:
320
+ logger.error(f"Failed to export commands: {e}")
321
+ return False
322
+
323
+ def import_commands(
324
+ self, import_path: Path, overwrite: bool = False
325
+ ) -> Dict[str, bool]:
326
+ """
327
+ Import commands from a JSON file.
328
+
329
+ Args:
330
+ import_path: Path to import file
331
+ overwrite: Whether to overwrite existing commands
332
+
333
+ Returns:
334
+ Dictionary mapping command names to success status
335
+ """
336
+ results = {}
337
+ try:
338
+ with open(import_path, "r") as f:
339
+ commands = json.load(f)
340
+
341
+ for command_data in commands:
342
+ name = command_data["name"]
343
+ command_file = self.commands_dir / f"{name}.json"
344
+
345
+ if command_file.exists() and not overwrite:
346
+ logger.warning(f"Command {name} already exists, skipping")
347
+ results[name] = False
348
+ continue
349
+
350
+ # Update timestamp
351
+ command_data["updated_at"] = datetime.utcnow().isoformat() + "Z"
352
+
353
+ with open(command_file, "w") as f:
354
+ json.dump(command_data, f, indent=2)
355
+
356
+ results[name] = True
357
+ logger.info(f"Imported command: {name}")
358
+
359
+ return results
360
+ except Exception as e:
361
+ logger.error(f"Failed to import commands: {e}")
362
+ return results
363
+
364
+
365
+ # Global instance
366
+ _command_manager: Optional[CustomCommandManager] = None
367
+
368
+
369
+ def get_command_manager() -> CustomCommandManager:
370
+ """Get the global custom command manager instance."""
371
+ global _command_manager
372
+ if _command_manager is None:
373
+ _command_manager = CustomCommandManager()
374
+ return _command_manager
375
+
376
+
377
+ def load_custom_commands(target_group: click.Group) -> int:
378
+ """
379
+ Load all custom commands and register them with the target Click group.
380
+
381
+ Args:
382
+ target_group: Click group to register commands with
383
+
384
+ Returns:
385
+ Number of commands successfully loaded
386
+ """
387
+ manager = get_command_manager()
388
+ commands = manager.load_all_commands()
389
+
390
+ loaded_count = 0
391
+ for command_data in commands:
392
+ # Check if command should be nested under a group
393
+ group_name = command_data.get("group")
394
+
395
+ if group_name:
396
+ # Find or create the group
397
+ group_cmd = target_group.commands.get(group_name)
398
+
399
+ # Handle LazyGroup - force loading
400
+ if group_cmd and hasattr(group_cmd, "_load_group"):
401
+ logger.debug(f"Loading lazy group: {group_name}")
402
+ group_cmd = group_cmd._load_group()
403
+ # Update the command in the parent group
404
+ target_group.commands[group_name] = group_cmd
405
+
406
+ if not group_cmd:
407
+ # Create the group if it doesn't exist
408
+ group_cmd = click.Group(name=group_name, help=f"{group_name.capitalize()} commands")
409
+ target_group.add_command(group_cmd)
410
+ logger.info(f"Created command group: {group_name}")
411
+
412
+ # Register the command under the group
413
+ if isinstance(group_cmd, click.Group):
414
+ if manager.register_command_with_click(command_data, group_cmd):
415
+ loaded_count += 1
416
+ else:
417
+ # Register at top level
418
+ if manager.register_command_with_click(command_data, target_group):
419
+ loaded_count += 1
420
+
421
+ if loaded_count > 0:
422
+ logger.info(f"Loaded {loaded_count} custom commands")
423
+
424
+ return loaded_count