mcli-framework 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,203 @@
1
+ import json
2
+ import time
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import requests
8
+
9
+ from mcli.lib.logger.logger import get_logger
10
+ from mcli.lib.toml.toml import read_from_toml
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class DaemonClientConfig:
17
+ """Configuration for daemon client"""
18
+
19
+ host: str = "localhost"
20
+ port: int = 8000
21
+ timeout: int = 30
22
+ retry_attempts: int = 3
23
+ retry_delay: float = 1.0
24
+
25
+
26
+ class APIDaemonClient:
27
+ """Client for interacting with the MCLI API Daemon or Flask shell daemon"""
28
+
29
+ def __init__(self, config: Optional[DaemonClientConfig] = None, shell_mode: bool = False):
30
+ self.config = config or self._load_config()
31
+ self.shell_mode = shell_mode
32
+ # If shell_mode, always use Flask test server at 0.0.0.0:5005
33
+ if shell_mode:
34
+ self.base_url = "http://localhost:5005"
35
+ else:
36
+ self.base_url = f"http://{self.config.host}:{self.config.port}"
37
+ self.session = requests.Session()
38
+
39
+ def execute_shell_command(self, command: str) -> Dict[str, Any]:
40
+ """Execute a raw shell command via the Flask test server"""
41
+ url = f"http://localhost:5005/execute"
42
+ try:
43
+ response = self.session.post(
44
+ url, json={"command": command}, timeout=self.config.timeout
45
+ )
46
+ response.raise_for_status()
47
+ return response.json()
48
+ except requests.exceptions.RequestException as e:
49
+ raise Exception(f"Failed to connect to Flask shell daemon at {url}: {e}")
50
+
51
+ def _load_config(self) -> DaemonClientConfig:
52
+ """Load configuration from config files"""
53
+ config = DaemonClientConfig()
54
+
55
+ # Try to load from config.toml files
56
+ config_paths = [
57
+ Path("config.toml"), # Current directory
58
+ Path.home() / ".config" / "mcli" / "config.toml", # User config
59
+ Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
60
+ ]
61
+
62
+ for path in config_paths:
63
+ if path.exists():
64
+ try:
65
+ daemon_config = read_from_toml(str(path), "api_daemon")
66
+ if daemon_config:
67
+ if daemon_config.get("host"):
68
+ config.host = daemon_config["host"]
69
+ if daemon_config.get("port"):
70
+ config.port = daemon_config["port"]
71
+ logger.debug(f"Loaded daemon client config from {path}")
72
+ break
73
+ except Exception as e:
74
+ logger.debug(f"Could not load daemon client config from {path}: {e}")
75
+
76
+ return config
77
+
78
+ def _make_request(
79
+ self,
80
+ method: str,
81
+ endpoint: str,
82
+ data: Optional[Dict] = None,
83
+ params: Optional[Dict] = None,
84
+ **kwargs,
85
+ ) -> Dict[str, Any]:
86
+ """Make HTTP request to daemon with retry logic"""
87
+ url = f"{self.base_url}{endpoint}"
88
+ for attempt in range(self.config.retry_attempts):
89
+ try:
90
+ if method.upper() == "GET":
91
+ response = self.session.request(
92
+ method=method, url=url, params=params, timeout=self.config.timeout, **kwargs
93
+ )
94
+ else:
95
+ response = self.session.request(
96
+ method=method, url=url, json=data, timeout=self.config.timeout, **kwargs
97
+ )
98
+ response.raise_for_status()
99
+ return response.json()
100
+ except requests.exceptions.RequestException as e:
101
+ if attempt == self.config.retry_attempts - 1:
102
+ raise Exception(f"Failed to connect to API daemon at {url}: {e}")
103
+ logger.warning(
104
+ f"Attempt {attempt + 1} failed, retrying in {self.config.retry_delay}s..."
105
+ )
106
+ time.sleep(self.config.retry_delay)
107
+ return {} # Always return a dict
108
+
109
+ def health_check(self) -> Dict[str, Any]:
110
+ """Check daemon health"""
111
+ return self._make_request("GET", "/health")
112
+
113
+ def status(self) -> Dict[str, Any]:
114
+ """Get daemon status"""
115
+ return self._make_request("GET", "/status")
116
+
117
+ def list_commands(self, all: bool = False) -> Dict[str, Any]:
118
+ """List available commands with metadata. If all=True, include inactive."""
119
+ params = {"all": str(all).lower()} if all else None
120
+ return self._make_request("GET", "/commands", params=params)
121
+
122
+ def get_command_details(self, command_id: str) -> Optional[Dict[str, Any]]:
123
+ """Get detailed information about a specific command"""
124
+ return self._make_request("GET", f"/commands/{command_id}")
125
+
126
+ def execute_command(
127
+ self,
128
+ command_id: Optional[str] = None,
129
+ command_name: Optional[str] = None,
130
+ args: Optional[List[str]] = None,
131
+ timeout: Optional[int] = None,
132
+ ) -> Dict[str, Any]:
133
+ """Execute a command via the daemon"""
134
+ if not command_id and not command_name:
135
+ raise ValueError("Either command_id or command_name must be provided")
136
+
137
+ data = {
138
+ "args": args or [],
139
+ }
140
+ if command_id is not None:
141
+ data["command_id"] = command_id
142
+ if command_name is not None:
143
+ data["command_name"] = command_name
144
+ if timeout is not None:
145
+ data["timeout"] = timeout
146
+ return self._make_request("POST", "/execute", data=data)
147
+
148
+ def start_daemon(self) -> Dict[str, Any]:
149
+ """Start the daemon via HTTP"""
150
+ return self._make_request("POST", "/daemon/start")
151
+
152
+ def stop_daemon(self) -> Dict[str, Any]:
153
+ """Stop the daemon via HTTP"""
154
+ return self._make_request("POST", "/daemon/stop")
155
+
156
+ def is_running(self) -> bool:
157
+ """Check if daemon is running"""
158
+ try:
159
+ status = self.status()
160
+ return status.get("running", False)
161
+ except Exception:
162
+ return False
163
+
164
+ def wait_for_daemon(self, timeout: int = 30) -> bool:
165
+ """Wait for daemon to be ready"""
166
+ start_time = time.time()
167
+ while time.time() - start_time < timeout:
168
+ if self.is_running():
169
+ return True
170
+ time.sleep(1)
171
+ return False
172
+
173
+
174
+ # Convenience functions for easy usage
175
+ def execute_shell_command_via_flask(command: str) -> Dict[str, Any]:
176
+ """Execute a raw shell command via the Flask test server (convenience function)"""
177
+ client = APIDaemonClient(shell_mode=True)
178
+ return client.execute_shell_command(command)
179
+
180
+
181
+ def get_daemon_client() -> APIDaemonClient:
182
+ """Get a configured daemon client"""
183
+ return APIDaemonClient()
184
+
185
+
186
+ def execute_command_via_daemon(
187
+ command_name: str, args: Optional[List[str]] = None
188
+ ) -> Dict[str, Any]:
189
+ """Execute a command via the daemon (convenience function)"""
190
+ client = get_daemon_client()
191
+ return client.execute_command(command_name=command_name, args=args)
192
+
193
+
194
+ def check_daemon_status() -> Dict[str, Any]:
195
+ """Check daemon status (convenience function)"""
196
+ client = get_daemon_client()
197
+ return client.status()
198
+
199
+
200
+ def list_available_commands() -> Dict[str, Any]:
201
+ """List available commands (convenience function)"""
202
+ client = get_daemon_client()
203
+ return client.list_commands()
@@ -0,0 +1,44 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from mcli.lib.logger.logger import get_logger
7
+ from mcli.lib.toml.toml import read_from_toml
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class LocalDaemonClient:
13
+ """Client for interacting with the MCLI Daemon via CLI subprocess (local IPC)"""
14
+
15
+ def __init__(self):
16
+ self.daemon_cmd = ["python", "-m", "mcli.workflow.daemon.daemon"]
17
+ # Optionally, you could use the installed CLI: ["mcli-daemon"]
18
+
19
+ def list_commands(self) -> Dict[str, Any]:
20
+ logger.info("[LocalDaemonClient] Invoking 'list-commands' via subprocess")
21
+ result = subprocess.run(
22
+ self.daemon_cmd + ["list-commands", "--json"], capture_output=True, text=True
23
+ )
24
+ logger.info(f"[LocalDaemonClient] stdout: {result.stdout}\nstderr: {result.stderr}")
25
+ if result.returncode != 0:
26
+ raise Exception(f"Daemon list-commands failed: {result.stderr}")
27
+ return json.loads(result.stdout)
28
+
29
+ def execute_command(
30
+ self, command_name: str, args: Optional[List[str]] = None
31
+ ) -> Dict[str, Any]:
32
+ logger.info(f"[LocalDaemonClient] Invoking 'execute' for {command_name} via subprocess")
33
+ cmd = self.daemon_cmd + ["execute", command_name] + (args or []) + ["--json"]
34
+ result = subprocess.run(cmd, capture_output=True, text=True)
35
+ logger.info(f"[LocalDaemonClient] stdout: {result.stdout}\nstderr: {result.stderr}")
36
+ if result.returncode != 0:
37
+ raise Exception(f"Daemon execute failed: {result.stderr}")
38
+ return json.loads(result.stdout)
39
+
40
+ # Add more methods as needed for other daemon features
41
+
42
+
43
+ def get_local_daemon_client():
44
+ return LocalDaemonClient()
@@ -0,0 +1,217 @@
1
+ import functools
2
+ import inspect
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Callable, Dict, List, Optional
6
+
7
+ from mcli.lib.logger.logger import get_logger
8
+ from mcli.lib.toml.toml import read_from_toml
9
+
10
+ from .daemon_client import APIDaemonClient, get_daemon_client
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def daemon_command(
16
+ command_name: Optional[str] = None,
17
+ auto_route: bool = True,
18
+ fallback_to_local: bool = True,
19
+ timeout: Optional[int] = None,
20
+ ):
21
+ """
22
+ Decorator to route Click commands to the API daemon when enabled.
23
+
24
+ Args:
25
+ command_name: Name of the command (defaults to function name)
26
+ auto_route: Whether to automatically route to daemon if enabled
27
+ fallback_to_local: Whether to fallback to local execution if daemon fails
28
+ timeout: Command timeout in seconds
29
+ """
30
+
31
+ def decorator(func: Callable) -> Callable:
32
+ # Get command name
33
+ cmd_name = command_name or func.__name__
34
+
35
+ @functools.wraps(func)
36
+ def wrapper(*args, **kwargs):
37
+ # Check if daemon routing is enabled
38
+ if not auto_route or not _is_daemon_routing_enabled():
39
+ return func(*args, **kwargs)
40
+
41
+ try:
42
+ # Try to execute via daemon
43
+ client = get_daemon_client()
44
+
45
+ # Convert args and kwargs to command arguments
46
+ cmd_args = _convert_to_command_args(args, kwargs, func)
47
+
48
+ # Execute via daemon
49
+ result = client.execute_command(
50
+ command_name=cmd_name, args=cmd_args, timeout=timeout
51
+ )
52
+
53
+ if result.get("success"):
54
+ logger.info(f"Command '{cmd_name}' executed successfully via daemon")
55
+ return result.get("result")
56
+ else:
57
+ logger.warning(
58
+ f"Daemon execution failed for '{cmd_name}': {result.get('error')}"
59
+ )
60
+ if fallback_to_local:
61
+ logger.info(f"Falling back to local execution for '{cmd_name}'")
62
+ return func(*args, **kwargs)
63
+ else:
64
+ raise Exception(f"Daemon execution failed: {result.get('error')}")
65
+
66
+ except Exception as e:
67
+ logger.warning(f"Failed to execute '{cmd_name}' via daemon: {e}")
68
+ if fallback_to_local:
69
+ logger.info(f"Falling back to local execution for '{cmd_name}'")
70
+ return func(*args, **kwargs)
71
+ else:
72
+ raise
73
+
74
+ return wrapper
75
+
76
+ return decorator
77
+
78
+
79
+ def _is_daemon_routing_enabled() -> bool:
80
+ """Check if daemon routing is enabled in configuration"""
81
+ # Check environment variable
82
+ if os.environ.get("MCLI_DAEMON_ROUTING", "false").lower() in ("true", "1", "yes"):
83
+ return True
84
+
85
+ # Check config files
86
+ config_paths = [
87
+ Path("config.toml"), # Current directory
88
+ Path.home() / ".config" / "mcli" / "config.toml", # User config
89
+ Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
90
+ ]
91
+
92
+ for path in config_paths:
93
+ if path.exists():
94
+ try:
95
+ daemon_config = read_from_toml(str(path), "api_daemon")
96
+ if daemon_config and daemon_config.get("enabled", False):
97
+ return True
98
+ except Exception as e:
99
+ logger.debug(f"Could not read daemon config from {path}: {e}")
100
+
101
+ return False
102
+
103
+
104
+ def _convert_to_command_args(args: tuple, kwargs: dict, func: Callable) -> List[str]:
105
+ """Convert function arguments to command line arguments"""
106
+ cmd_args = []
107
+
108
+ # Get function signature
109
+ sig = inspect.signature(func)
110
+ bound_args = sig.bind(*args, **kwargs)
111
+ bound_args.apply_defaults()
112
+
113
+ # Convert to command line arguments
114
+ for param_name, param_value in bound_args.arguments.items():
115
+ param = sig.parameters[param_name]
116
+
117
+ # Skip self parameter for methods
118
+ if param_name == "self":
119
+ continue
120
+
121
+ # Handle different parameter types
122
+ if param.kind == inspect.Parameter.POSITIONAL_ONLY:
123
+ # Positional argument
124
+ if param_value is not None:
125
+ cmd_args.append(str(param_value))
126
+ elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
127
+ # Can be positional or keyword
128
+ if param_value is not None:
129
+ if param.default == inspect.Parameter.empty:
130
+ # Required argument
131
+ cmd_args.append(str(param_value))
132
+ else:
133
+ # Optional argument with value
134
+ cmd_args.extend([f"--{param_name}", str(param_value)])
135
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
136
+ # Keyword-only argument
137
+ if param_value is not None and param_value != param.default:
138
+ cmd_args.extend([f"--{param_name}", str(param_value)])
139
+ elif param.kind == inspect.Parameter.VAR_POSITIONAL:
140
+ # *args
141
+ if isinstance(param_value, (list, tuple)):
142
+ cmd_args.extend([str(arg) for arg in param_value])
143
+ elif param.kind == inspect.Parameter.VAR_KEYWORD:
144
+ # **kwargs
145
+ if isinstance(param_value, dict):
146
+ for key, value in param_value.items():
147
+ cmd_args.extend([f"--{key}", str(value)])
148
+
149
+ return cmd_args
150
+
151
+
152
+ def daemon_group(
153
+ group_name: Optional[str] = None, auto_route: bool = True, fallback_to_local: bool = True
154
+ ):
155
+ """
156
+ Decorator to route Click groups to the API daemon when enabled.
157
+
158
+ Args:
159
+ group_name: Name of the group (defaults to function name)
160
+ auto_route: Whether to automatically route to daemon if enabled
161
+ fallback_to_local: Whether to fallback to local execution if daemon fails
162
+ """
163
+
164
+ def decorator(func: Callable) -> Callable:
165
+ # Get group name
166
+ grp_name = group_name or func.__name__
167
+
168
+ @functools.wraps(func)
169
+ def wrapper(*args, **kwargs):
170
+ # Check if daemon routing is enabled
171
+ if not auto_route or not _is_daemon_routing_enabled():
172
+ return func(*args, **kwargs)
173
+
174
+ try:
175
+ # Try to execute via daemon
176
+ client = get_daemon_client()
177
+
178
+ # For groups, we'll just check if the daemon is available
179
+ # and then fall back to local execution
180
+ if client.is_running():
181
+ logger.info(
182
+ f"Daemon is running, but group '{grp_name}' will be executed locally"
183
+ )
184
+
185
+ return func(*args, **kwargs)
186
+
187
+ except Exception as e:
188
+ logger.warning(f"Failed to check daemon for group '{grp_name}': {e}")
189
+ if fallback_to_local:
190
+ logger.info(f"Falling back to local execution for group '{grp_name}'")
191
+ return func(*args, **kwargs)
192
+ else:
193
+ raise
194
+
195
+ return wrapper
196
+
197
+ return decorator
198
+
199
+
200
+ # Convenience function to check if daemon is available
201
+ def is_daemon_available() -> bool:
202
+ """Check if the API daemon is available and running"""
203
+ try:
204
+ client = get_daemon_client()
205
+ return client.is_running()
206
+ except Exception:
207
+ return False
208
+
209
+
210
+ # Convenience function to get daemon status
211
+ def get_daemon_status() -> Optional[Dict[str, Any]]:
212
+ """Get daemon status if available"""
213
+ try:
214
+ client = get_daemon_client()
215
+ return client.status()
216
+ except Exception:
217
+ return None