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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
"""
|
|
2
|
+
System Controller for MCLI Chat
|
|
3
|
+
Allows the chat interface to directly control system applications and execute commands
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from mcli.lib.logger.logger import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SystemController:
|
|
20
|
+
"""Handles real-time system control for MCLI chat"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.system = platform.system()
|
|
24
|
+
self.enabled = True
|
|
25
|
+
self.current_directory = os.getcwd() # Track current working directory
|
|
26
|
+
|
|
27
|
+
def execute_command(self, command: str, description: str = "") -> Dict[str, Any]:
|
|
28
|
+
"""Execute a system command and return results"""
|
|
29
|
+
try:
|
|
30
|
+
logger.info(f"Executing: {description or command}")
|
|
31
|
+
|
|
32
|
+
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"success": result.returncode == 0,
|
|
36
|
+
"output": result.stdout,
|
|
37
|
+
"error": result.stderr,
|
|
38
|
+
"command": command,
|
|
39
|
+
"description": description,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
return {
|
|
44
|
+
"success": False,
|
|
45
|
+
"output": "",
|
|
46
|
+
"error": "Command timed out after 30 seconds",
|
|
47
|
+
"command": command,
|
|
48
|
+
"description": description,
|
|
49
|
+
}
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return {
|
|
52
|
+
"success": False,
|
|
53
|
+
"output": "",
|
|
54
|
+
"error": str(e),
|
|
55
|
+
"command": command,
|
|
56
|
+
"description": description,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def open_textedit_and_write(self, text: str, filename: str = None) -> Dict[str, Any]:
|
|
60
|
+
"""Open TextEdit, write text, and optionally save to file"""
|
|
61
|
+
if self.system != "Darwin":
|
|
62
|
+
return {
|
|
63
|
+
"success": False,
|
|
64
|
+
"error": "TextEdit is only available on macOS",
|
|
65
|
+
"description": "Open TextEdit and write text",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Escape quotes in text for AppleScript
|
|
69
|
+
escaped_text = text.replace('"', '\\"').replace("'", "\\'")
|
|
70
|
+
|
|
71
|
+
if filename:
|
|
72
|
+
# Save to specified file
|
|
73
|
+
save_path = f'(path to desktop as string) & "{filename}"'
|
|
74
|
+
save_command = f"save front document in {save_path}"
|
|
75
|
+
else:
|
|
76
|
+
# Just create new document without saving
|
|
77
|
+
save_command = ""
|
|
78
|
+
|
|
79
|
+
applescript = f"""
|
|
80
|
+
tell application "TextEdit"
|
|
81
|
+
activate
|
|
82
|
+
delay 0.5
|
|
83
|
+
make new document
|
|
84
|
+
delay 0.5
|
|
85
|
+
set text of front document to "{escaped_text}"
|
|
86
|
+
delay 1
|
|
87
|
+
{save_command}
|
|
88
|
+
end tell
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
return self.execute_command(
|
|
92
|
+
f"osascript -e '{applescript}'", f"Open TextEdit and write: {text[:50]}..."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def control_application(self, app_name: str, action: str, **kwargs) -> Dict[str, Any]:
|
|
96
|
+
"""Control various applications with different actions"""
|
|
97
|
+
|
|
98
|
+
if self.system == "Darwin": # macOS
|
|
99
|
+
return self._control_macos_app(app_name, action, **kwargs)
|
|
100
|
+
elif self.system == "Windows":
|
|
101
|
+
return self._control_windows_app(app_name, action, **kwargs)
|
|
102
|
+
else: # Linux
|
|
103
|
+
return self._control_linux_app(app_name, action, **kwargs)
|
|
104
|
+
|
|
105
|
+
def _control_macos_app(self, app_name: str, action: str, **kwargs) -> Dict[str, Any]:
|
|
106
|
+
"""Control macOS applications using AppleScript"""
|
|
107
|
+
|
|
108
|
+
if action == "open":
|
|
109
|
+
applescript = f'tell application "{app_name}" to activate'
|
|
110
|
+
|
|
111
|
+
elif action == "close":
|
|
112
|
+
applescript = f'tell application "{app_name}" to quit'
|
|
113
|
+
|
|
114
|
+
elif action == "write_text" and app_name.lower() == "textedit":
|
|
115
|
+
text = kwargs.get("text", "Hello, World!")
|
|
116
|
+
filename = kwargs.get("filename")
|
|
117
|
+
return self.open_textedit_and_write(text, filename)
|
|
118
|
+
|
|
119
|
+
elif action == "new_document":
|
|
120
|
+
if app_name.lower() == "textedit":
|
|
121
|
+
applescript = f"""
|
|
122
|
+
tell application "{app_name}"
|
|
123
|
+
activate
|
|
124
|
+
delay 0.5
|
|
125
|
+
make new document
|
|
126
|
+
end tell
|
|
127
|
+
"""
|
|
128
|
+
else:
|
|
129
|
+
applescript = f'tell application "{app_name}" to make new document'
|
|
130
|
+
|
|
131
|
+
elif action == "type_text":
|
|
132
|
+
text = kwargs.get("text", "Hello, World!")
|
|
133
|
+
escaped_text = text.replace('"', '\\"')
|
|
134
|
+
applescript = f"""
|
|
135
|
+
tell application "System Events"
|
|
136
|
+
tell process "{app_name}"
|
|
137
|
+
keystroke "{escaped_text}"
|
|
138
|
+
end tell
|
|
139
|
+
end tell
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
return {
|
|
144
|
+
"success": False,
|
|
145
|
+
"error": f"Unknown action '{action}' for {app_name}",
|
|
146
|
+
"description": f"Control {app_name}",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return self.execute_command(f"osascript -e '{applescript}'", f"{action} {app_name}")
|
|
150
|
+
|
|
151
|
+
def _control_windows_app(self, app_name: str, action: str, **kwargs) -> Dict[str, Any]:
|
|
152
|
+
"""Control Windows applications using PowerShell"""
|
|
153
|
+
|
|
154
|
+
if action == "open":
|
|
155
|
+
# Try to start the application
|
|
156
|
+
command = f"powershell -Command \"Start-Process '{app_name}'\""
|
|
157
|
+
|
|
158
|
+
elif action == "close":
|
|
159
|
+
command = f"powershell -Command \"Stop-Process -Name '{app_name}' -Force\""
|
|
160
|
+
|
|
161
|
+
elif action == "write_text":
|
|
162
|
+
text = kwargs.get("text", "Hello, World!")
|
|
163
|
+
# This is a simplified version - would need more complex implementation
|
|
164
|
+
command = f"powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{text}')\""
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
return {
|
|
168
|
+
"success": False,
|
|
169
|
+
"error": f"Action '{action}' not implemented for Windows",
|
|
170
|
+
"description": f"Control {app_name}",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return self.execute_command(command, f"{action} {app_name}")
|
|
174
|
+
|
|
175
|
+
def _control_linux_app(self, app_name: str, action: str, **kwargs) -> Dict[str, Any]:
|
|
176
|
+
"""Control Linux applications using various tools"""
|
|
177
|
+
|
|
178
|
+
if action == "open":
|
|
179
|
+
# Try different methods to open applications
|
|
180
|
+
commands = [
|
|
181
|
+
f"{app_name.lower()}",
|
|
182
|
+
f"gtk-launch {app_name.lower()}",
|
|
183
|
+
f"xdg-open {app_name.lower()}",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
for cmd in commands:
|
|
187
|
+
result = self.execute_command(cmd, f"Open {app_name}")
|
|
188
|
+
if result["success"]:
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"success": False,
|
|
193
|
+
"error": f"Could not open {app_name}",
|
|
194
|
+
"description": f"Open {app_name}",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
elif action == "close":
|
|
198
|
+
command = f"pkill -f {app_name.lower()}"
|
|
199
|
+
|
|
200
|
+
elif action == "write_text":
|
|
201
|
+
text = kwargs.get("text", "Hello, World!")
|
|
202
|
+
# Use xdotool if available
|
|
203
|
+
command = f'xdotool type "{text}"'
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
return {
|
|
207
|
+
"success": False,
|
|
208
|
+
"error": f"Action '{action}' not implemented for Linux",
|
|
209
|
+
"description": f"Control {app_name}",
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return self.execute_command(command, f"{action} {app_name}")
|
|
213
|
+
|
|
214
|
+
def get_system_info(self) -> Dict[str, Any]:
|
|
215
|
+
"""Get comprehensive system information"""
|
|
216
|
+
try:
|
|
217
|
+
import platform
|
|
218
|
+
from datetime import datetime
|
|
219
|
+
|
|
220
|
+
import psutil
|
|
221
|
+
|
|
222
|
+
# Basic system info
|
|
223
|
+
info = {
|
|
224
|
+
"timestamp": datetime.now().isoformat(),
|
|
225
|
+
"system": platform.system(),
|
|
226
|
+
"platform": platform.platform(),
|
|
227
|
+
"machine": platform.machine(),
|
|
228
|
+
"processor": platform.processor(),
|
|
229
|
+
"python_version": platform.python_version(),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# CPU information
|
|
233
|
+
cpu_info = {
|
|
234
|
+
"physical_cores": psutil.cpu_count(logical=False) or 0,
|
|
235
|
+
"logical_cores": psutil.cpu_count(logical=True) or 0,
|
|
236
|
+
"cpu_usage_percent": psutil.cpu_percent(
|
|
237
|
+
interval=0.1
|
|
238
|
+
), # Shorter interval to avoid hanging
|
|
239
|
+
"cpu_frequency": None,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Try to get CPU frequency safely
|
|
243
|
+
try:
|
|
244
|
+
freq = psutil.cpu_freq()
|
|
245
|
+
if freq:
|
|
246
|
+
cpu_info["cpu_frequency"] = freq._asdict()
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
info["cpu"] = cpu_info
|
|
250
|
+
|
|
251
|
+
# Memory information
|
|
252
|
+
memory = psutil.virtual_memory()
|
|
253
|
+
memory_info = {
|
|
254
|
+
"total_gb": round(memory.total / (1024**3), 2),
|
|
255
|
+
"available_gb": round(memory.available / (1024**3), 2),
|
|
256
|
+
"used_gb": round(memory.used / (1024**3), 2),
|
|
257
|
+
"usage_percent": memory.percent,
|
|
258
|
+
"free_gb": round(memory.free / (1024**3), 2),
|
|
259
|
+
}
|
|
260
|
+
info["memory"] = memory_info
|
|
261
|
+
|
|
262
|
+
# Disk information
|
|
263
|
+
disk_info = []
|
|
264
|
+
try:
|
|
265
|
+
for partition in psutil.disk_partitions():
|
|
266
|
+
try:
|
|
267
|
+
partition_usage = psutil.disk_usage(partition.mountpoint)
|
|
268
|
+
disk_info.append(
|
|
269
|
+
{
|
|
270
|
+
"device": partition.device,
|
|
271
|
+
"mountpoint": partition.mountpoint,
|
|
272
|
+
"filesystem": partition.fstype,
|
|
273
|
+
"total_gb": round(partition_usage.total / (1024**3), 2),
|
|
274
|
+
"used_gb": round(partition_usage.used / (1024**3), 2),
|
|
275
|
+
"free_gb": round(partition_usage.free / (1024**3), 2),
|
|
276
|
+
"usage_percent": round(
|
|
277
|
+
(partition_usage.used / partition_usage.total) * 100, 1
|
|
278
|
+
),
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
except (PermissionError, OSError):
|
|
282
|
+
# Skip partitions we can't access
|
|
283
|
+
continue
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
info["disks"] = disk_info
|
|
287
|
+
|
|
288
|
+
# Network information
|
|
289
|
+
network_info = {
|
|
290
|
+
"interfaces": {},
|
|
291
|
+
"connections": 0,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
network_info["connections"] = len(psutil.net_connections())
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
for interface, addresses in psutil.net_if_addrs().items():
|
|
301
|
+
network_info["interfaces"][interface] = []
|
|
302
|
+
for addr in addresses:
|
|
303
|
+
try:
|
|
304
|
+
network_info["interfaces"][interface].append(
|
|
305
|
+
{
|
|
306
|
+
"family": str(addr.family),
|
|
307
|
+
"address": addr.address,
|
|
308
|
+
"netmask": addr.netmask,
|
|
309
|
+
"broadcast": addr.broadcast,
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
except Exception:
|
|
313
|
+
continue
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
info["network"] = network_info
|
|
318
|
+
|
|
319
|
+
# Process information
|
|
320
|
+
process_info = {
|
|
321
|
+
"total_processes": 0,
|
|
322
|
+
"running_processes": 0,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
process_info["total_processes"] = len(psutil.pids())
|
|
327
|
+
# Count running processes more safely
|
|
328
|
+
running_count = 0
|
|
329
|
+
for proc in psutil.process_iter(["status"]):
|
|
330
|
+
try:
|
|
331
|
+
if proc.info["status"] == psutil.STATUS_RUNNING:
|
|
332
|
+
running_count += 1
|
|
333
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
334
|
+
continue
|
|
335
|
+
process_info["running_processes"] = running_count
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
info["processes"] = process_info
|
|
339
|
+
|
|
340
|
+
# Boot time
|
|
341
|
+
try:
|
|
342
|
+
import datetime
|
|
343
|
+
|
|
344
|
+
boot_time = datetime.datetime.fromtimestamp(psutil.boot_time())
|
|
345
|
+
info["boot_time"] = boot_time.isoformat()
|
|
346
|
+
uptime_seconds = (datetime.datetime.now() - boot_time).total_seconds()
|
|
347
|
+
info["uptime_hours"] = round(uptime_seconds / 3600, 2)
|
|
348
|
+
except Exception:
|
|
349
|
+
info["boot_time"] = None
|
|
350
|
+
info["uptime_hours"] = 0
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"success": True,
|
|
354
|
+
"data": info,
|
|
355
|
+
"description": "System information retrieved successfully",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
except ImportError:
|
|
359
|
+
return {
|
|
360
|
+
"success": False,
|
|
361
|
+
"error": "psutil library not available. Install with: pip install psutil",
|
|
362
|
+
"description": "Get system information",
|
|
363
|
+
}
|
|
364
|
+
except Exception as e:
|
|
365
|
+
return {"success": False, "error": str(e), "description": "Get system information"}
|
|
366
|
+
|
|
367
|
+
def get_system_time(self) -> Dict[str, Any]:
|
|
368
|
+
"""Get current system time and timezone information"""
|
|
369
|
+
try:
|
|
370
|
+
import time
|
|
371
|
+
from datetime import datetime
|
|
372
|
+
|
|
373
|
+
now = datetime.now()
|
|
374
|
+
|
|
375
|
+
time_info = {
|
|
376
|
+
"current_time": now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
377
|
+
"timezone": time.tzname,
|
|
378
|
+
"timestamp": now.timestamp(),
|
|
379
|
+
"iso_format": now.isoformat(),
|
|
380
|
+
"day_of_week": now.strftime("%A"),
|
|
381
|
+
"day_of_year": now.timetuple().tm_yday,
|
|
382
|
+
"week_number": now.isocalendar().week,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {"success": True, "data": time_info, "description": "Current system time"}
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
return {"success": False, "error": str(e), "description": "Get system time"}
|
|
389
|
+
|
|
390
|
+
def get_memory_usage(self) -> Dict[str, Any]:
|
|
391
|
+
"""Get detailed memory usage information"""
|
|
392
|
+
try:
|
|
393
|
+
import psutil
|
|
394
|
+
|
|
395
|
+
# Virtual memory
|
|
396
|
+
memory = psutil.virtual_memory()
|
|
397
|
+
|
|
398
|
+
# Swap memory
|
|
399
|
+
swap = psutil.swap_memory()
|
|
400
|
+
|
|
401
|
+
memory_info = {
|
|
402
|
+
"virtual_memory": {
|
|
403
|
+
"total_gb": round(memory.total / (1024**3), 2),
|
|
404
|
+
"available_gb": round(memory.available / (1024**3), 2),
|
|
405
|
+
"used_gb": round(memory.used / (1024**3), 2),
|
|
406
|
+
"free_gb": round(memory.free / (1024**3), 2),
|
|
407
|
+
"usage_percent": memory.percent,
|
|
408
|
+
"buffers_gb": round(getattr(memory, "buffers", 0) / (1024**3), 2),
|
|
409
|
+
"cached_gb": round(getattr(memory, "cached", 0) / (1024**3), 2),
|
|
410
|
+
},
|
|
411
|
+
"swap_memory": {
|
|
412
|
+
"total_gb": round(swap.total / (1024**3), 2),
|
|
413
|
+
"used_gb": round(swap.used / (1024**3), 2),
|
|
414
|
+
"free_gb": round(swap.free / (1024**3), 2),
|
|
415
|
+
"usage_percent": swap.percent,
|
|
416
|
+
},
|
|
417
|
+
"recommendations": [],
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Add cleanup recommendations
|
|
421
|
+
if memory.percent > 80:
|
|
422
|
+
memory_info["recommendations"].append(
|
|
423
|
+
"High memory usage detected - consider closing unused applications"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if memory.percent > 90:
|
|
427
|
+
memory_info["recommendations"].append(
|
|
428
|
+
"Critical memory usage - immediate cleanup recommended"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if swap.percent > 50:
|
|
432
|
+
memory_info["recommendations"].append("High swap usage - consider adding more RAM")
|
|
433
|
+
|
|
434
|
+
return {"success": True, "data": memory_info, "description": "Memory usage information"}
|
|
435
|
+
|
|
436
|
+
except ImportError:
|
|
437
|
+
return {
|
|
438
|
+
"success": False,
|
|
439
|
+
"error": "psutil library not available",
|
|
440
|
+
"description": "Get memory usage",
|
|
441
|
+
}
|
|
442
|
+
except Exception as e:
|
|
443
|
+
return {"success": False, "error": str(e), "description": "Get memory usage"}
|
|
444
|
+
|
|
445
|
+
def get_disk_usage(self) -> Dict[str, Any]:
|
|
446
|
+
"""Get detailed disk usage information"""
|
|
447
|
+
try:
|
|
448
|
+
import psutil
|
|
449
|
+
|
|
450
|
+
disk_info = {
|
|
451
|
+
"partitions": [],
|
|
452
|
+
"total_disk_gb": 0,
|
|
453
|
+
"total_used_gb": 0,
|
|
454
|
+
"total_free_gb": 0,
|
|
455
|
+
"recommendations": [],
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for partition in psutil.disk_partitions():
|
|
459
|
+
try:
|
|
460
|
+
usage = psutil.disk_usage(partition.mountpoint)
|
|
461
|
+
|
|
462
|
+
partition_info = {
|
|
463
|
+
"device": partition.device,
|
|
464
|
+
"mountpoint": partition.mountpoint,
|
|
465
|
+
"filesystem": partition.fstype,
|
|
466
|
+
"total_gb": round(usage.total / (1024**3), 2),
|
|
467
|
+
"used_gb": round(usage.used / (1024**3), 2),
|
|
468
|
+
"free_gb": round(usage.free / (1024**3), 2),
|
|
469
|
+
"usage_percent": round((usage.used / usage.total) * 100, 1),
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
disk_info["partitions"].append(partition_info)
|
|
473
|
+
|
|
474
|
+
# Add to totals (main disk only)
|
|
475
|
+
if partition.mountpoint == "/" or partition.mountpoint == "C:\\":
|
|
476
|
+
disk_info["total_disk_gb"] = partition_info["total_gb"]
|
|
477
|
+
disk_info["total_used_gb"] = partition_info["used_gb"]
|
|
478
|
+
disk_info["total_free_gb"] = partition_info["free_gb"]
|
|
479
|
+
|
|
480
|
+
# Add recommendations
|
|
481
|
+
if partition_info["usage_percent"] > 85:
|
|
482
|
+
disk_info["recommendations"].append(
|
|
483
|
+
f"Disk {partition.mountpoint} is {partition_info['usage_percent']}% full - cleanup recommended"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if partition_info["usage_percent"] > 95:
|
|
487
|
+
disk_info["recommendations"].append(
|
|
488
|
+
f"Critical: Disk {partition.mountpoint} is nearly full!"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
except PermissionError:
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
return {"success": True, "data": disk_info, "description": "Disk usage information"}
|
|
495
|
+
|
|
496
|
+
except ImportError:
|
|
497
|
+
return {
|
|
498
|
+
"success": False,
|
|
499
|
+
"error": "psutil library not available",
|
|
500
|
+
"description": "Get disk usage",
|
|
501
|
+
}
|
|
502
|
+
except Exception as e:
|
|
503
|
+
return {"success": False, "error": str(e), "description": "Get disk usage"}
|
|
504
|
+
|
|
505
|
+
def clear_system_caches(self) -> Dict[str, Any]:
|
|
506
|
+
"""Clear system caches and temporary files"""
|
|
507
|
+
try:
|
|
508
|
+
cleared_items = []
|
|
509
|
+
total_freed_mb = 0
|
|
510
|
+
|
|
511
|
+
if self.system == "Darwin": # macOS
|
|
512
|
+
# Clear user caches
|
|
513
|
+
cache_dirs = [
|
|
514
|
+
"~/Library/Caches",
|
|
515
|
+
"~/Library/Application Support/CrashReporter",
|
|
516
|
+
"/tmp",
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
for cache_dir in cache_dirs:
|
|
520
|
+
expanded_dir = os.path.expanduser(cache_dir)
|
|
521
|
+
if os.path.exists(expanded_dir):
|
|
522
|
+
try:
|
|
523
|
+
# Calculate size before clearing
|
|
524
|
+
size_before = self._get_directory_size(expanded_dir)
|
|
525
|
+
|
|
526
|
+
# Clear cache (be careful with system directories)
|
|
527
|
+
if cache_dir == "/tmp":
|
|
528
|
+
self.execute_command(
|
|
529
|
+
"find /tmp -type f -atime +1 -delete 2>/dev/null || true",
|
|
530
|
+
"Clear old temp files",
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
# Just clear user cache files that are safe to delete
|
|
534
|
+
self.execute_command(
|
|
535
|
+
f"find '{expanded_dir}' -name '*.cache' -delete 2>/dev/null || true",
|
|
536
|
+
f"Clear {cache_dir}",
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
size_after = self._get_directory_size(expanded_dir)
|
|
540
|
+
freed_mb = max(0, (size_before - size_after) / (1024 * 1024))
|
|
541
|
+
|
|
542
|
+
if freed_mb > 0:
|
|
543
|
+
cleared_items.append(f"Cleared {cache_dir}: {freed_mb:.1f} MB")
|
|
544
|
+
total_freed_mb += freed_mb
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
cleared_items.append(f"Could not clear {cache_dir}: {e}")
|
|
548
|
+
|
|
549
|
+
# Clear DNS cache
|
|
550
|
+
dns_result = self.execute_command(
|
|
551
|
+
"sudo dscacheutil -flushcache 2>/dev/null || dscacheutil -flushcache",
|
|
552
|
+
"Flush DNS cache",
|
|
553
|
+
)
|
|
554
|
+
if dns_result.get("success"):
|
|
555
|
+
cleared_items.append("Flushed DNS cache")
|
|
556
|
+
|
|
557
|
+
elif self.system == "Windows":
|
|
558
|
+
# Windows cache clearing
|
|
559
|
+
commands = [
|
|
560
|
+
("del /q /f /s %TEMP%\\*", "Clear temp files"),
|
|
561
|
+
("del /q /f /s C:\\Windows\\Temp\\*", "Clear Windows temp"),
|
|
562
|
+
("ipconfig /flushdns", "Flush DNS cache"),
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
for cmd, desc in commands:
|
|
566
|
+
result = self.execute_command(cmd, desc)
|
|
567
|
+
if result.get("success"):
|
|
568
|
+
cleared_items.append(desc)
|
|
569
|
+
|
|
570
|
+
else: # Linux
|
|
571
|
+
# Linux cache clearing
|
|
572
|
+
commands = [
|
|
573
|
+
(
|
|
574
|
+
"find /tmp -type f -atime +1 -delete 2>/dev/null || true",
|
|
575
|
+
"Clear old temp files",
|
|
576
|
+
),
|
|
577
|
+
(
|
|
578
|
+
"sync && echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true",
|
|
579
|
+
"Clear page cache",
|
|
580
|
+
),
|
|
581
|
+
]
|
|
582
|
+
|
|
583
|
+
for cmd, desc in commands:
|
|
584
|
+
result = self.execute_command(cmd, desc)
|
|
585
|
+
if result.get("success"):
|
|
586
|
+
cleared_items.append(desc)
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
"success": True,
|
|
590
|
+
"data": {
|
|
591
|
+
"cleared_items": cleared_items,
|
|
592
|
+
"total_freed_mb": round(total_freed_mb, 1),
|
|
593
|
+
},
|
|
594
|
+
"description": "System cache clearing",
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
except Exception as e:
|
|
598
|
+
return {"success": False, "error": str(e), "description": "Clear system caches"}
|
|
599
|
+
|
|
600
|
+
def _get_directory_size(self, path: str) -> int:
|
|
601
|
+
"""Get total size of directory in bytes"""
|
|
602
|
+
total_size = 0
|
|
603
|
+
try:
|
|
604
|
+
for dirpath, dirnames, filenames in os.walk(path):
|
|
605
|
+
for filename in filenames:
|
|
606
|
+
file_path = os.path.join(dirpath, filename)
|
|
607
|
+
try:
|
|
608
|
+
total_size += os.path.getsize(file_path)
|
|
609
|
+
except (OSError, FileNotFoundError):
|
|
610
|
+
continue
|
|
611
|
+
except Exception:
|
|
612
|
+
pass
|
|
613
|
+
return total_size
|
|
614
|
+
|
|
615
|
+
def get_running_applications(self) -> List[str]:
|
|
616
|
+
"""Get list of currently running applications"""
|
|
617
|
+
try:
|
|
618
|
+
if self.system == "Darwin":
|
|
619
|
+
result = subprocess.run(
|
|
620
|
+
[
|
|
621
|
+
"osascript",
|
|
622
|
+
"-e",
|
|
623
|
+
'tell application "System Events" to get name of every process whose background only is false',
|
|
624
|
+
],
|
|
625
|
+
capture_output=True,
|
|
626
|
+
text=True,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
if result.returncode == 0:
|
|
630
|
+
# Parse the AppleScript result
|
|
631
|
+
apps = result.stdout.strip().split(", ")
|
|
632
|
+
return [app.strip() for app in apps if app.strip()]
|
|
633
|
+
|
|
634
|
+
elif self.system == "Windows":
|
|
635
|
+
result = subprocess.run(
|
|
636
|
+
[
|
|
637
|
+
"powershell",
|
|
638
|
+
"-Command",
|
|
639
|
+
"Get-Process | Where-Object {$_.MainWindowTitle -ne ''} | Select-Object -ExpandProperty ProcessName",
|
|
640
|
+
],
|
|
641
|
+
capture_output=True,
|
|
642
|
+
text=True,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
if result.returncode == 0:
|
|
646
|
+
return [line.strip() for line in result.stdout.split("\n") if line.strip()]
|
|
647
|
+
|
|
648
|
+
else: # Linux
|
|
649
|
+
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
|
650
|
+
|
|
651
|
+
if result.returncode == 0:
|
|
652
|
+
lines = result.stdout.split("\n")[1:] # Skip header
|
|
653
|
+
processes = []
|
|
654
|
+
for line in lines:
|
|
655
|
+
if line.strip():
|
|
656
|
+
parts = line.split()
|
|
657
|
+
if len(parts) > 10:
|
|
658
|
+
processes.append(parts[10]) # Command name
|
|
659
|
+
return list(set(processes))
|
|
660
|
+
|
|
661
|
+
except Exception as e:
|
|
662
|
+
logger.error(f"Error getting running applications: {e}")
|
|
663
|
+
|
|
664
|
+
return []
|
|
665
|
+
|
|
666
|
+
def take_screenshot(self, filename: str = None) -> Dict[str, Any]:
|
|
667
|
+
"""Take a screenshot and save it"""
|
|
668
|
+
if not filename:
|
|
669
|
+
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
670
|
+
filename = f"mcli_screenshot_{timestamp}.png"
|
|
671
|
+
|
|
672
|
+
desktop_path = Path.home() / "Desktop" / filename
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
if self.system == "Darwin":
|
|
676
|
+
command = f"screencapture -x '{desktop_path}'"
|
|
677
|
+
elif self.system == "Windows":
|
|
678
|
+
command = f"powershell -Command \"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Screen]::PrimaryScreen.Bounds | Out-Null; [System.Drawing.Bitmap]::new([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height).Save('{desktop_path}', [System.Drawing.Imaging.ImageFormat]::Png)\""
|
|
679
|
+
else: # Linux
|
|
680
|
+
command = f"gnome-screenshot -f '{desktop_path}'"
|
|
681
|
+
|
|
682
|
+
result = self.execute_command(command, f"Take screenshot: {filename}")
|
|
683
|
+
|
|
684
|
+
if result["success"]:
|
|
685
|
+
result["screenshot_path"] = str(desktop_path)
|
|
686
|
+
|
|
687
|
+
return result
|
|
688
|
+
|
|
689
|
+
except Exception as e:
|
|
690
|
+
return {
|
|
691
|
+
"success": False,
|
|
692
|
+
"error": str(e),
|
|
693
|
+
"description": f"Take screenshot: {filename}",
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
def open_file_or_url(self, path_or_url: str) -> Dict[str, Any]:
|
|
697
|
+
"""Open a file or URL using the system default application"""
|
|
698
|
+
try:
|
|
699
|
+
if self.system == "Darwin":
|
|
700
|
+
command = f"open '{path_or_url}'"
|
|
701
|
+
elif self.system == "Windows":
|
|
702
|
+
command = f'start "" "{path_or_url}"'
|
|
703
|
+
else: # Linux
|
|
704
|
+
command = f"xdg-open '{path_or_url}'"
|
|
705
|
+
|
|
706
|
+
return self.execute_command(command, f"Open: {path_or_url}")
|
|
707
|
+
|
|
708
|
+
except Exception as e:
|
|
709
|
+
return {"success": False, "error": str(e), "description": f"Open: {path_or_url}"}
|
|
710
|
+
|
|
711
|
+
def change_directory(self, path: str) -> Dict[str, Any]:
|
|
712
|
+
"""Navigate to a directory and update current working directory"""
|
|
713
|
+
try:
|
|
714
|
+
# Expand user path and resolve relative paths
|
|
715
|
+
expanded_path = os.path.expanduser(path)
|
|
716
|
+
resolved_path = os.path.abspath(expanded_path)
|
|
717
|
+
|
|
718
|
+
if not os.path.exists(resolved_path):
|
|
719
|
+
return {
|
|
720
|
+
"success": False,
|
|
721
|
+
"error": f"Directory does not exist: {resolved_path}",
|
|
722
|
+
"current_directory": self.current_directory,
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if not os.path.isdir(resolved_path):
|
|
726
|
+
return {
|
|
727
|
+
"success": False,
|
|
728
|
+
"error": f"Path is not a directory: {resolved_path}",
|
|
729
|
+
"current_directory": self.current_directory,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# Change directory
|
|
733
|
+
os.chdir(resolved_path)
|
|
734
|
+
self.current_directory = resolved_path
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
"success": True,
|
|
738
|
+
"current_directory": self.current_directory,
|
|
739
|
+
"message": f"Changed to directory: {self.current_directory}",
|
|
740
|
+
"description": f"Navigate to {path}",
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
return {
|
|
745
|
+
"success": False,
|
|
746
|
+
"error": str(e),
|
|
747
|
+
"current_directory": self.current_directory,
|
|
748
|
+
"description": f"Navigate to {path}",
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
def list_directory(
|
|
752
|
+
self, path: str = None, show_hidden: bool = False, detailed: bool = False
|
|
753
|
+
) -> Dict[str, Any]:
|
|
754
|
+
"""List contents of a directory"""
|
|
755
|
+
try:
|
|
756
|
+
target_path = path if path else self.current_directory
|
|
757
|
+
expanded_path = os.path.expanduser(target_path)
|
|
758
|
+
resolved_path = os.path.abspath(expanded_path)
|
|
759
|
+
|
|
760
|
+
if not os.path.exists(resolved_path):
|
|
761
|
+
return {
|
|
762
|
+
"success": False,
|
|
763
|
+
"error": f"Directory does not exist: {resolved_path}",
|
|
764
|
+
"current_directory": self.current_directory,
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if not os.path.isdir(resolved_path):
|
|
768
|
+
return {
|
|
769
|
+
"success": False,
|
|
770
|
+
"error": f"Path is not a directory: {resolved_path}",
|
|
771
|
+
"current_directory": self.current_directory,
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
# Get directory contents
|
|
775
|
+
entries = []
|
|
776
|
+
try:
|
|
777
|
+
for item in os.listdir(resolved_path):
|
|
778
|
+
if not show_hidden and item.startswith("."):
|
|
779
|
+
continue
|
|
780
|
+
|
|
781
|
+
item_path = os.path.join(resolved_path, item)
|
|
782
|
+
stat_info = os.stat(item_path)
|
|
783
|
+
|
|
784
|
+
entry = {
|
|
785
|
+
"name": item,
|
|
786
|
+
"type": "directory" if os.path.isdir(item_path) else "file",
|
|
787
|
+
"size": stat_info.st_size if not os.path.isdir(item_path) else None,
|
|
788
|
+
"modified": datetime.fromtimestamp(stat_info.st_mtime).isoformat(),
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if detailed:
|
|
792
|
+
entry.update(
|
|
793
|
+
{
|
|
794
|
+
"permissions": oct(stat_info.st_mode)[-3:],
|
|
795
|
+
"owner": stat_info.st_uid,
|
|
796
|
+
"group": stat_info.st_gid,
|
|
797
|
+
}
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
entries.append(entry)
|
|
801
|
+
|
|
802
|
+
# Sort: directories first, then files
|
|
803
|
+
entries.sort(key=lambda x: (x["type"] != "directory", x["name"]))
|
|
804
|
+
|
|
805
|
+
except PermissionError:
|
|
806
|
+
return {
|
|
807
|
+
"success": False,
|
|
808
|
+
"error": f"Permission denied accessing: {resolved_path}",
|
|
809
|
+
"current_directory": self.current_directory,
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return {
|
|
813
|
+
"success": True,
|
|
814
|
+
"path": resolved_path,
|
|
815
|
+
"entries": entries,
|
|
816
|
+
"current_directory": self.current_directory,
|
|
817
|
+
"description": f"List directory: {resolved_path}",
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
return {
|
|
822
|
+
"success": False,
|
|
823
|
+
"error": str(e),
|
|
824
|
+
"current_directory": self.current_directory,
|
|
825
|
+
"description": f"List directory: {path or 'current'}",
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
def clean_simulator_data(self) -> Dict[str, Any]:
|
|
829
|
+
"""Clean iOS/watchOS simulator data specifically"""
|
|
830
|
+
try:
|
|
831
|
+
if self.system != "Darwin":
|
|
832
|
+
return {
|
|
833
|
+
"success": False,
|
|
834
|
+
"error": "Simulator cleanup is only available on macOS",
|
|
835
|
+
"description": "Clean simulator data",
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
simulator_base = os.path.expanduser("~/Library/Developer/CoreSimulator")
|
|
839
|
+
if not os.path.exists(simulator_base):
|
|
840
|
+
return {
|
|
841
|
+
"success": False,
|
|
842
|
+
"error": "No simulator data found",
|
|
843
|
+
"description": "Clean simulator data",
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
cleaned_items = []
|
|
847
|
+
total_freed_bytes = 0
|
|
848
|
+
|
|
849
|
+
# Paths to clean
|
|
850
|
+
cleanup_paths = [
|
|
851
|
+
"Devices/*/data/Containers/Data/Application/*/Library/Caches",
|
|
852
|
+
"Devices/*/data/Containers/Data/Application/*/tmp",
|
|
853
|
+
"Devices/*/data/Library/Caches",
|
|
854
|
+
"Devices/*/data/tmp",
|
|
855
|
+
]
|
|
856
|
+
|
|
857
|
+
import glob
|
|
858
|
+
|
|
859
|
+
for pattern in cleanup_paths:
|
|
860
|
+
full_pattern = os.path.join(simulator_base, pattern)
|
|
861
|
+
matching_paths = glob.glob(full_pattern)
|
|
862
|
+
|
|
863
|
+
for cache_path in matching_paths:
|
|
864
|
+
try:
|
|
865
|
+
# Calculate size before deletion
|
|
866
|
+
size_before = self._get_directory_size(cache_path)
|
|
867
|
+
|
|
868
|
+
# Remove cache contents but keep directory structure
|
|
869
|
+
if os.path.isdir(cache_path):
|
|
870
|
+
import shutil
|
|
871
|
+
|
|
872
|
+
for item in os.listdir(cache_path):
|
|
873
|
+
item_path = os.path.join(cache_path, item)
|
|
874
|
+
if os.path.isfile(item_path):
|
|
875
|
+
os.remove(item_path)
|
|
876
|
+
elif os.path.isdir(item_path):
|
|
877
|
+
shutil.rmtree(item_path, ignore_errors=True)
|
|
878
|
+
|
|
879
|
+
total_freed_bytes += size_before
|
|
880
|
+
cleaned_items.append(f"Cleaned {cache_path}")
|
|
881
|
+
|
|
882
|
+
except Exception as e:
|
|
883
|
+
cleaned_items.append(f"Could not clean {cache_path}: {e}")
|
|
884
|
+
|
|
885
|
+
# Also run xcrun simctl to clean old unavailable simulators
|
|
886
|
+
try:
|
|
887
|
+
unavailable_result = self.execute_command(
|
|
888
|
+
"xcrun simctl delete unavailable", "Remove unavailable simulators"
|
|
889
|
+
)
|
|
890
|
+
if unavailable_result.get("success"):
|
|
891
|
+
cleaned_items.append("Removed unavailable simulator runtimes")
|
|
892
|
+
except Exception:
|
|
893
|
+
pass
|
|
894
|
+
|
|
895
|
+
# Convert bytes to MB
|
|
896
|
+
total_freed_mb = total_freed_bytes / (1024 * 1024)
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
"success": True,
|
|
900
|
+
"data": {
|
|
901
|
+
"cleaned_items": cleaned_items,
|
|
902
|
+
"total_freed_mb": round(total_freed_mb, 1),
|
|
903
|
+
"freed_bytes": total_freed_bytes,
|
|
904
|
+
},
|
|
905
|
+
"message": f"Simulator cleanup completed. Freed {total_freed_mb:.1f} MB",
|
|
906
|
+
"description": "Clean simulator data",
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
except Exception as e:
|
|
910
|
+
return {"success": False, "error": str(e), "description": "Clean simulator data"}
|
|
911
|
+
|
|
912
|
+
def execute_shell_command(self, command: str, working_directory: str = None) -> Dict[str, Any]:
|
|
913
|
+
"""Execute a shell command in a specific directory"""
|
|
914
|
+
try:
|
|
915
|
+
# Use working directory if specified, otherwise use current directory
|
|
916
|
+
cwd = working_directory if working_directory else self.current_directory
|
|
917
|
+
|
|
918
|
+
logger.info(f"Executing shell command in {cwd}: {command}")
|
|
919
|
+
|
|
920
|
+
result = subprocess.run(
|
|
921
|
+
command, shell=True, capture_output=True, text=True, timeout=60, cwd=cwd
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
"success": result.returncode == 0,
|
|
926
|
+
"output": result.stdout,
|
|
927
|
+
"error": result.stderr,
|
|
928
|
+
"command": command,
|
|
929
|
+
"working_directory": cwd,
|
|
930
|
+
"current_directory": self.current_directory,
|
|
931
|
+
"description": f"Execute: {command}",
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
except subprocess.TimeoutExpired:
|
|
935
|
+
return {
|
|
936
|
+
"success": False,
|
|
937
|
+
"output": "",
|
|
938
|
+
"error": "Command timed out after 60 seconds",
|
|
939
|
+
"command": command,
|
|
940
|
+
"working_directory": cwd,
|
|
941
|
+
"current_directory": self.current_directory,
|
|
942
|
+
"description": f"Execute: {command}",
|
|
943
|
+
}
|
|
944
|
+
except Exception as e:
|
|
945
|
+
return {
|
|
946
|
+
"success": False,
|
|
947
|
+
"output": "",
|
|
948
|
+
"error": str(e),
|
|
949
|
+
"command": command,
|
|
950
|
+
"working_directory": cwd if "cwd" in locals() else self.current_directory,
|
|
951
|
+
"current_directory": self.current_directory,
|
|
952
|
+
"description": f"Execute: {command}",
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# Global instance for use in chat
|
|
957
|
+
system_controller = SystemController()
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
# Helper functions for easy use in chat
|
|
961
|
+
def open_textedit_and_write(text: str, filename: str = None) -> Dict[str, Any]:
|
|
962
|
+
"""Helper function to open TextEdit and write text"""
|
|
963
|
+
return system_controller.open_textedit_and_write(text, filename)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def control_app(app_name: str, action: str, **kwargs) -> Dict[str, Any]:
|
|
967
|
+
"""Helper function to control applications"""
|
|
968
|
+
return system_controller.control_application(app_name, action, **kwargs)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def execute_system_command(command: str, description: str = "") -> Dict[str, Any]:
|
|
972
|
+
"""Helper function to execute system commands"""
|
|
973
|
+
return system_controller.execute_command(command, description)
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def take_screenshot(filename: str = None) -> Dict[str, Any]:
|
|
977
|
+
"""Helper function to take screenshots"""
|
|
978
|
+
return system_controller.take_screenshot(filename)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def open_file_or_url(path_or_url: str) -> Dict[str, Any]:
|
|
982
|
+
"""Helper function to open files or URLs"""
|
|
983
|
+
return system_controller.open_file_or_url(path_or_url)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def change_directory(path: str) -> Dict[str, Any]:
|
|
987
|
+
"""Helper function to navigate to a directory"""
|
|
988
|
+
return system_controller.change_directory(path)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def list_directory(
|
|
992
|
+
path: str = None, show_hidden: bool = False, detailed: bool = False
|
|
993
|
+
) -> Dict[str, Any]:
|
|
994
|
+
"""Helper function to list directory contents"""
|
|
995
|
+
return system_controller.list_directory(path, show_hidden, detailed)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def clean_simulator_data() -> Dict[str, Any]:
|
|
999
|
+
"""Helper function to clean iOS/watchOS simulator data"""
|
|
1000
|
+
return system_controller.clean_simulator_data()
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def execute_shell_command(command: str, working_directory: str = None) -> Dict[str, Any]:
|
|
1004
|
+
"""Helper function to execute shell commands"""
|
|
1005
|
+
return system_controller.execute_shell_command(command, working_directory)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def get_current_directory() -> str:
|
|
1009
|
+
"""Helper function to get current working directory"""
|
|
1010
|
+
return system_controller.current_directory
|