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.
- mcli/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/main.py +10 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/custom_commands.py +424 -0
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/paths.py +12 -0
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/api/schemas.py +2 -2
- mcli/ml/auth/__init__.py +45 -0
- mcli/ml/auth/models.py +2 -2
- mcli/ml/backtesting/__init__.py +39 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/cli/main.py +1 -1
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/app.py +13 -13
- mcli/ml/dashboard/app_integrated.py +1309 -148
- mcli/ml/dashboard/app_supabase.py +46 -21
- mcli/ml/dashboard/app_training.py +14 -14
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/components/charts.py +258 -0
- mcli/ml/dashboard/components/metrics.py +125 -0
- mcli/ml/dashboard/components/tables.py +228 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/cicd.py +382 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
- mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
- mcli/ml/dashboard/pages/test_portfolio.py +373 -0
- mcli/ml/dashboard/pages/trading.py +714 -0
- mcli/ml/dashboard/pages/workflows.py +533 -0
- mcli/ml/dashboard/utils.py +154 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +33 -0
- mcli/ml/models/__init__.py +94 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +60 -0
- mcli/ml/trading/alpaca_client.py +353 -0
- mcli/ml/trading/migrations.py +164 -0
- mcli/ml/trading/models.py +418 -0
- mcli/ml/trading/paper_trading.py +326 -0
- mcli/ml/trading/risk_management.py +370 -0
- mcli/ml/trading/trading_service.py +480 -0
- mcli/ml/training/__init__.py +10 -0
- mcli/ml/training/train_model.py +569 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/self_cmd.py +579 -91
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/daemon/daemon.py +21 -3
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +4 -0
- mcli/workflow/politician_trading/data_sources.py +259 -1
- mcli/workflow/politician_trading/models.py +159 -1
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
- mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
- mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
- mcli/workflow/politician_trading/seed_database.py +539 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- mcli/workflow/workflow.py +8 -27
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
- mcli/workflow/daemon/api_daemon.py +0 -800
- mcli/workflow/daemon/commands.py +0 -1196
- mcli/workflow/dashboard/dashboard_cmd.py +0 -120
- mcli/workflow/file/file.py +0 -100
- mcli/workflow/git_commit/commands.py +0 -430
- mcli/workflow/politician_trading/commands.py +0 -1939
- mcli/workflow/scheduler/commands.py +0 -493
- mcli/workflow/sync/sync_cmd.py +0 -437
- mcli/workflow/videos/videos.py +0 -242
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
mcli/lib/api/__init__.py
ADDED
|
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
|