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,905 @@
1
+ import json
2
+ import os
3
+ import signal
4
+ import sqlite3
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import time
9
+ import uuid
10
+ from dataclasses import asdict, dataclass
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional, Union
14
+
15
+ import click
16
+ import psutil
17
+ from sklearn.feature_extraction.text import TfidfVectorizer
18
+ from sklearn.metrics.pairwise import cosine_similarity
19
+ from watchdog.events import FileSystemEventHandler
20
+ from watchdog.observers import Observer
21
+
22
+ # Import existing utilities
23
+ from mcli.lib.logger.logger import get_logger
24
+ from mcli.lib.toml.toml import read_from_toml
25
+ from mcli.workflow.daemon.commands import CommandDatabase
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ @dataclass
31
+ class Command:
32
+ """Represents a stored command"""
33
+
34
+ id: str
35
+ name: str
36
+ description: str
37
+ code: str
38
+ language: str # 'python', 'node', 'lua', 'shell'
39
+ group: Optional[str] = None
40
+ tags: Optional[List[str]] = None
41
+ created_at: Optional[datetime] = None
42
+ updated_at: Optional[datetime] = None
43
+ execution_count: int = 0
44
+ last_executed: Optional[datetime] = None
45
+ is_active: bool = True
46
+
47
+
48
+ class CommandFileWatcher(FileSystemEventHandler):
49
+ """Watches a directory for command file changes and updates the registry."""
50
+
51
+ def __init__(self, db, watch_dir: str):
52
+ self.db = db
53
+ self.watch_dir = Path(watch_dir)
54
+ self.observer = Observer()
55
+ self.observer.schedule(self, str(self.watch_dir), recursive=False)
56
+ self.observer.start()
57
+
58
+ def on_modified(self, event):
59
+ if event.is_directory:
60
+ return
61
+ self._reload_command_file(event.src_path)
62
+
63
+ def on_created(self, event):
64
+ if event.is_directory:
65
+ return
66
+ self._reload_command_file(event.src_path)
67
+
68
+ def on_deleted(self, event):
69
+ if event.is_directory:
70
+ return
71
+ # Remove command from DB if file deleted
72
+ cmd_id = Path(event.src_path).stem
73
+ self.db.delete_command(cmd_id)
74
+
75
+ def _reload_command_file(self, file_path):
76
+ # Example: expects each file to be a JSON with command fields
77
+ try:
78
+ with open(file_path, "r") as f:
79
+ data = json.load(f)
80
+ cmd = Command(
81
+ id=data["id"],
82
+ name=data["name"],
83
+ description=data.get("description", ""),
84
+ code=data["code"],
85
+ language=data["language"],
86
+ group=data.get("group"),
87
+ tags=data.get("tags", []),
88
+ created_at=(
89
+ datetime.fromisoformat(data["created_at"])
90
+ if data.get("created_at")
91
+ else datetime.now()
92
+ ),
93
+ updated_at=(
94
+ datetime.fromisoformat(data["updated_at"])
95
+ if data.get("updated_at")
96
+ else datetime.now()
97
+ ),
98
+ execution_count=data.get("execution_count", 0),
99
+ last_executed=(
100
+ datetime.fromisoformat(data["last_executed"])
101
+ if data.get("last_executed")
102
+ else None
103
+ ),
104
+ is_active=data.get("is_active", True),
105
+ )
106
+ # Upsert: try update, else add
107
+ if not self.db.update_command(cmd):
108
+ self.db.add_command(cmd)
109
+ except Exception as e:
110
+ logger.error(f"Failed to reload command file {file_path}: {e}")
111
+
112
+
113
+ def start_command_file_watcher(db, watch_dir: str = None):
114
+ """Start a file watcher for command files (JSON) in a directory."""
115
+ if watch_dir is None:
116
+ watch_dir = str(Path.home() / ".local" / "mcli" / "daemon" / "commands")
117
+ Path(watch_dir).mkdir(parents=True, exist_ok=True)
118
+ watcher = CommandFileWatcher(db, watch_dir)
119
+ logger.info(f"Started command file watcher on {watch_dir}")
120
+ return watcher
121
+ """Manages command storage and retrieval"""
122
+
123
+ def __init__(self, db_path: Optional[str] = None):
124
+ if db_path is None:
125
+ db_path = Path.home() / ".local" / "mcli" / "daemon" / "commands.db"
126
+
127
+ self.db_path = Path(db_path)
128
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
129
+ self.init_database()
130
+
131
+ # Initialize vectorizer for similarity search
132
+ self.vectorizer = TfidfVectorizer(
133
+ max_features=1000, stop_words="english", ngram_range=(1, 2)
134
+ )
135
+ self._update_embeddings()
136
+
137
+ def init_database(self):
138
+ """Initialize SQLite database"""
139
+ conn = sqlite3.connect(self.db_path)
140
+ cursor = conn.cursor()
141
+
142
+ # Commands table
143
+ cursor.execute(
144
+ """
145
+ CREATE TABLE IF NOT EXISTS commands (
146
+ id TEXT PRIMARY KEY,
147
+ name TEXT NOT NULL,
148
+ description TEXT,
149
+ code TEXT NOT NULL,
150
+ language TEXT NOT NULL,
151
+ group_name TEXT,
152
+ tags TEXT,
153
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
154
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
155
+ execution_count INTEGER DEFAULT 0,
156
+ last_executed TIMESTAMP,
157
+ is_active BOOLEAN DEFAULT 1
158
+ )
159
+ """
160
+ )
161
+
162
+ # Groups table for hierarchical organization
163
+ cursor.execute(
164
+ """
165
+ CREATE TABLE IF NOT EXISTS groups (
166
+ id TEXT PRIMARY KEY,
167
+ name TEXT NOT NULL,
168
+ description TEXT,
169
+ parent_group_id TEXT,
170
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
171
+ FOREIGN KEY (parent_group_id) REFERENCES groups (id)
172
+ )
173
+ """
174
+ )
175
+
176
+ # Execution history
177
+ cursor.execute(
178
+ """
179
+ CREATE TABLE IF NOT EXISTS executions (
180
+ id TEXT PRIMARY KEY,
181
+ command_id TEXT NOT NULL,
182
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
183
+ status TEXT NOT NULL,
184
+ output TEXT,
185
+ error TEXT,
186
+ execution_time_ms INTEGER,
187
+ FOREIGN KEY (command_id) REFERENCES commands (id)
188
+ )
189
+ """
190
+ )
191
+
192
+ conn.commit()
193
+ conn.close()
194
+
195
+ def _update_embeddings(self):
196
+ """Update TF-IDF embeddings for similarity search"""
197
+ commands = self.get_all_commands()
198
+ if not commands:
199
+ return
200
+
201
+ # Combine name, description, and tags for embedding
202
+ texts = []
203
+ for cmd in commands:
204
+ text_parts = [cmd.name, cmd.description or ""]
205
+ text_parts.extend(cmd.tags or [])
206
+ texts.append(" ".join(text_parts))
207
+
208
+ if texts:
209
+ self.vectorizer.fit(texts)
210
+
211
+ def add_command(self, command: Command) -> str:
212
+ """Add a new command to the database"""
213
+ conn = sqlite3.connect(self.db_path)
214
+ cursor = conn.cursor()
215
+
216
+ try:
217
+ cursor.execute(
218
+ """
219
+ INSERT INTO commands
220
+ (id, name, description, code, language, group_name, tags,
221
+ created_at, updated_at, execution_count, last_executed, is_active)
222
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
223
+ """,
224
+ (
225
+ command.id,
226
+ command.name,
227
+ command.description,
228
+ command.code,
229
+ command.language,
230
+ command.group,
231
+ json.dumps(command.tags),
232
+ command.created_at.isoformat(),
233
+ command.updated_at.isoformat(),
234
+ command.execution_count,
235
+ command.last_executed.isoformat() if command.last_executed else None,
236
+ command.is_active,
237
+ ),
238
+ )
239
+
240
+ conn.commit()
241
+ self._update_embeddings()
242
+ return command.id
243
+
244
+ except Exception as e:
245
+ logger.error(f"Error adding command: {e}")
246
+ conn.rollback()
247
+ raise
248
+ finally:
249
+ conn.close()
250
+
251
+ def get_command(self, command_id: str) -> Optional[Command]:
252
+ """Get a command by ID"""
253
+ conn = sqlite3.connect(self.db_path)
254
+ cursor = conn.cursor()
255
+
256
+ try:
257
+ cursor.execute(
258
+ """
259
+ SELECT id, name, description, code, language, group_name, tags,
260
+ created_at, updated_at, execution_count, last_executed, is_active
261
+ FROM commands WHERE id = ?
262
+ """,
263
+ (command_id,),
264
+ )
265
+
266
+ row = cursor.fetchone()
267
+ if row:
268
+ return self._row_to_command(row)
269
+ return None
270
+
271
+ finally:
272
+ conn.close()
273
+
274
+ def get_all_commands(self, include_inactive: bool = False) -> List[Command]:
275
+ """Get all commands, optionally including inactive ones"""
276
+ conn = sqlite3.connect(self.db_path)
277
+ cursor = conn.cursor()
278
+ try:
279
+ if include_inactive:
280
+ cursor.execute(
281
+ """
282
+ SELECT id, name, description, code, language, group_name, tags,
283
+ created_at, updated_at, execution_count, last_executed, is_active
284
+ FROM commands
285
+ ORDER BY name
286
+ """
287
+ )
288
+ else:
289
+ cursor.execute(
290
+ """
291
+ SELECT id, name, description, code, language, group_name, tags,
292
+ created_at, updated_at, execution_count, last_executed, is_active
293
+ FROM commands WHERE is_active = 1
294
+ ORDER BY name
295
+ """
296
+ )
297
+ return [self._row_to_command(row) for row in cursor.fetchall()]
298
+ finally:
299
+ conn.close()
300
+
301
+ def search_commands(self, query: str, limit: int = 10) -> List[Command]:
302
+ """Search commands by name, description, or tags"""
303
+ conn = sqlite3.connect(self.db_path)
304
+ cursor = conn.cursor()
305
+
306
+ try:
307
+ # Simple text search
308
+ search_term = f"%{query}%"
309
+ cursor.execute(
310
+ """
311
+ SELECT id, name, description, code, language, group_name, tags,
312
+ created_at, updated_at, execution_count, last_executed, is_active
313
+ FROM commands
314
+ WHERE is_active = 1
315
+ AND (name LIKE ? OR description LIKE ? OR tags LIKE ?)
316
+ ORDER BY name
317
+ LIMIT ?
318
+ """,
319
+ (search_term, search_term, search_term, limit),
320
+ )
321
+
322
+ return [self._row_to_command(row) for row in cursor.fetchall()]
323
+
324
+ finally:
325
+ conn.close()
326
+
327
+ def find_similar_commands(self, query: str, limit: int = 5) -> List[tuple]:
328
+ """Find similar commands using cosine similarity"""
329
+ commands = self.get_all_commands()
330
+ if not commands:
331
+ return []
332
+
333
+ # Prepare query text
334
+ query_text = query.lower()
335
+
336
+ # Get command texts for comparison
337
+ command_texts = []
338
+ for cmd in commands:
339
+ text_parts = [cmd.name, cmd.description or ""]
340
+ text_parts.extend(cmd.tags or [])
341
+ command_texts.append(" ".join(text_parts).lower())
342
+
343
+ if not command_texts:
344
+ return []
345
+
346
+ # Calculate similarities
347
+ try:
348
+ # Transform query and commands
349
+ query_vector = self.vectorizer.transform([query_text])
350
+ command_vectors = self.vectorizer.transform(command_texts)
351
+
352
+ # Calculate cosine similarities
353
+ similarities = cosine_similarity(query_vector, command_vectors).flatten()
354
+
355
+ # Sort by similarity
356
+ command_similarities = list(zip(commands, similarities))
357
+ command_similarities.sort(key=lambda x: x[1], reverse=True)
358
+
359
+ return command_similarities[:limit]
360
+
361
+ except Exception as e:
362
+ logger.error(f"Error calculating similarities: {e}")
363
+ return []
364
+
365
+ def update_command(self, command: Command) -> bool:
366
+ """Update an existing command"""
367
+ conn = sqlite3.connect(self.db_path)
368
+ cursor = conn.cursor()
369
+
370
+ try:
371
+ cursor.execute(
372
+ """
373
+ UPDATE commands
374
+ SET name = ?, description = ?, code = ?, language = ?,
375
+ group_name = ?, tags = ?, updated_at = ?, is_active = ?
376
+ WHERE id = ?
377
+ """,
378
+ (
379
+ command.name,
380
+ command.description,
381
+ command.code,
382
+ command.language,
383
+ command.group,
384
+ json.dumps(command.tags),
385
+ datetime.now().isoformat(),
386
+ command.is_active,
387
+ command.id,
388
+ ),
389
+ )
390
+
391
+ conn.commit()
392
+ self._update_embeddings()
393
+ return cursor.rowcount > 0
394
+
395
+ except Exception as e:
396
+ logger.error(f"Error updating command: {e}")
397
+ conn.rollback()
398
+ return False
399
+ finally:
400
+ conn.close()
401
+
402
+ def delete_command(self, command_id: str) -> bool:
403
+ """Delete a command (soft delete)"""
404
+ conn = sqlite3.connect(self.db_path)
405
+ cursor = conn.cursor()
406
+
407
+ try:
408
+ cursor.execute(
409
+ """
410
+ UPDATE commands SET is_active = 0 WHERE id = ?
411
+ """,
412
+ (command_id,),
413
+ )
414
+
415
+ conn.commit()
416
+ self._update_embeddings()
417
+ return cursor.rowcount > 0
418
+
419
+ except Exception as e:
420
+ logger.error(f"Error deleting command: {e}")
421
+ conn.rollback()
422
+ return False
423
+ finally:
424
+ conn.close()
425
+
426
+ def record_execution(
427
+ self,
428
+ command_id: str,
429
+ status: str,
430
+ output: str = None,
431
+ error: str = None,
432
+ execution_time_ms: int = None,
433
+ ):
434
+ """Record command execution"""
435
+ conn = sqlite3.connect(self.db_path)
436
+ cursor = conn.cursor()
437
+
438
+ try:
439
+ # Record execution
440
+ execution_id = str(uuid.uuid4())
441
+ cursor.execute(
442
+ """
443
+ INSERT INTO executions
444
+ (id, command_id, executed_at, status, output, error, execution_time_ms)
445
+ VALUES (?, ?, ?, ?, ?, ?, ?)
446
+ """,
447
+ (
448
+ execution_id,
449
+ command_id,
450
+ datetime.now().isoformat(),
451
+ status,
452
+ output,
453
+ error,
454
+ execution_time_ms,
455
+ ),
456
+ )
457
+
458
+ # Update command stats
459
+ cursor.execute(
460
+ """
461
+ UPDATE commands
462
+ SET execution_count = execution_count + 1,
463
+ last_executed = ?
464
+ WHERE id = ?
465
+ """,
466
+ (datetime.now().isoformat(), command_id),
467
+ )
468
+
469
+ conn.commit()
470
+
471
+ except Exception as e:
472
+ logger.error(f"Error recording execution: {e}")
473
+ conn.rollback()
474
+ finally:
475
+ conn.close()
476
+
477
+ def _row_to_command(self, row) -> Command:
478
+ """Convert database row to Command object"""
479
+ return Command(
480
+ id=row[0],
481
+ name=row[1],
482
+ description=row[2],
483
+ code=row[3],
484
+ language=row[4],
485
+ group=row[5],
486
+ tags=json.loads(row[6]) if row[6] else [],
487
+ created_at=datetime.fromisoformat(row[7]),
488
+ updated_at=datetime.fromisoformat(row[8]),
489
+ execution_count=row[9],
490
+ last_executed=datetime.fromisoformat(row[10]) if row[10] else None,
491
+ is_active=bool(row[11]),
492
+ )
493
+
494
+
495
+ class CommandExecutor:
496
+ """Handles safe execution of commands in different languages"""
497
+
498
+ def __init__(self, temp_dir: Optional[str] = None):
499
+ self.temp_dir = Path(temp_dir) if temp_dir else Path(tempfile.gettempdir()) / "mcli_daemon"
500
+ self.temp_dir.mkdir(parents=True, exist_ok=True)
501
+
502
+ # Language-specific execution environments
503
+ self.language_handlers = {
504
+ "python": self._execute_python,
505
+ "node": self._execute_node,
506
+ "lua": self._execute_lua,
507
+ "shell": self._execute_shell,
508
+ }
509
+
510
+ def execute_command(self, command: Command, args: List[str] = None) -> Dict[str, Any]:
511
+ """Execute a command safely"""
512
+ start_time = time.time()
513
+
514
+ try:
515
+ # Get the appropriate handler
516
+ handler = self.language_handlers.get(command.language)
517
+ if not handler:
518
+ raise ValueError(f"Unsupported language: {command.language}")
519
+
520
+ # Execute the command
521
+ result = handler(command, args or [])
522
+
523
+ execution_time = int((time.time() - start_time) * 1000)
524
+
525
+ return {
526
+ "success": True,
527
+ "output": result.get("output", ""),
528
+ "error": result.get("error", ""),
529
+ "execution_time_ms": execution_time,
530
+ "status": "completed",
531
+ }
532
+
533
+ except Exception as e:
534
+ execution_time = int((time.time() - start_time) * 1000)
535
+ return {
536
+ "success": False,
537
+ "output": "",
538
+ "error": str(e),
539
+ "execution_time_ms": execution_time,
540
+ "status": "failed",
541
+ }
542
+
543
+ def _execute_python(self, command: Command, args: List[str]) -> Dict[str, str]:
544
+ """Execute Python code safely"""
545
+ # Create temporary file
546
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.py"
547
+
548
+ try:
549
+ # Write code to file
550
+ with open(script_file, "w") as f:
551
+ f.write(command.code)
552
+
553
+ # Execute with subprocess
554
+ result = subprocess.run(
555
+ [sys.executable, str(script_file)] + args,
556
+ capture_output=True,
557
+ text=True,
558
+ timeout=30, # 30 second timeout
559
+ cwd=self.temp_dir,
560
+ )
561
+
562
+ return {"output": result.stdout, "error": result.stderr}
563
+
564
+ finally:
565
+ # Clean up
566
+ if script_file.exists():
567
+ script_file.unlink()
568
+
569
+ def _execute_node(self, command: Command, args: List[str]) -> Dict[str, str]:
570
+ """Execute Node.js code safely"""
571
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.js"
572
+
573
+ try:
574
+ with open(script_file, "w") as f:
575
+ f.write(command.code)
576
+
577
+ result = subprocess.run(
578
+ ["node", str(script_file)] + args,
579
+ capture_output=True,
580
+ text=True,
581
+ timeout=30,
582
+ cwd=self.temp_dir,
583
+ )
584
+
585
+ return {"output": result.stdout, "error": result.stderr}
586
+
587
+ finally:
588
+ if script_file.exists():
589
+ script_file.unlink()
590
+
591
+ def _execute_lua(self, command: Command, args: List[str]) -> Dict[str, str]:
592
+ """Execute Lua code safely"""
593
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.lua"
594
+
595
+ try:
596
+ with open(script_file, "w") as f:
597
+ f.write(command.code)
598
+
599
+ result = subprocess.run(
600
+ ["lua", str(script_file)] + args,
601
+ capture_output=True,
602
+ text=True,
603
+ timeout=30,
604
+ cwd=self.temp_dir,
605
+ )
606
+
607
+ return {"output": result.stdout, "error": result.stderr}
608
+
609
+ finally:
610
+ if script_file.exists():
611
+ script_file.unlink()
612
+
613
+ def _execute_shell(self, command: Command, args: List[str]) -> Dict[str, str]:
614
+ """Execute shell commands safely"""
615
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.sh"
616
+
617
+ try:
618
+ with open(script_file, "w") as f:
619
+ f.write("#!/bin/bash\n")
620
+ f.write(command.code)
621
+
622
+ # Make executable
623
+ script_file.chmod(0o755)
624
+
625
+ result = subprocess.run(
626
+ [str(script_file)] + args,
627
+ capture_output=True,
628
+ text=True,
629
+ timeout=30,
630
+ cwd=self.temp_dir,
631
+ )
632
+
633
+ return {"output": result.stdout, "error": result.stderr}
634
+
635
+ finally:
636
+ if script_file.exists():
637
+ script_file.unlink()
638
+
639
+
640
+ class DaemonService:
641
+ """Background daemon service for command management"""
642
+
643
+ def __init__(self, config_path: Optional[str] = None):
644
+ # Load configuration from TOML
645
+ if config_path is None:
646
+ # Try to find config.toml in common locations
647
+ config_paths = [
648
+ Path("config.toml"), # Current directory
649
+ Path.home() / ".config" / "mcli" / "config.toml", # User config
650
+ Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
651
+ ]
652
+
653
+ for path in config_paths:
654
+ if path.exists():
655
+ config_path = str(path)
656
+ break
657
+
658
+ self.config = {}
659
+ if config_path:
660
+ try:
661
+ # Load paths configuration
662
+ paths_config = read_from_toml(config_path, "paths")
663
+ if paths_config:
664
+ self.config["paths"] = paths_config
665
+ logger.info(f"Loaded config from {config_path}")
666
+ except Exception as e:
667
+ logger.warning(f"Could not load config from {config_path}: {e}")
668
+
669
+ self.db = CommandDatabase()
670
+ self.executor = CommandExecutor()
671
+ self.running = False
672
+ self.pid_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.pid"
673
+ self.socket_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.sock"
674
+
675
+ # Ensure daemon directory exists
676
+ self.pid_file.parent.mkdir(parents=True, exist_ok=True)
677
+
678
+ def start(self):
679
+ """Start the daemon service"""
680
+ if self.running:
681
+ logger.info("Daemon is already running")
682
+ return
683
+
684
+ # Check if already running
685
+ if self.pid_file.exists():
686
+ try:
687
+ with open(self.pid_file, "r") as f:
688
+ pid = int(f.read().strip())
689
+ if psutil.pid_exists(pid):
690
+ logger.info(f"Daemon already running with PID {pid}")
691
+ return
692
+ except Exception:
693
+ pass
694
+
695
+ # Start daemon
696
+ self.running = True
697
+
698
+ # Write PID file
699
+ with open(self.pid_file, "w") as f:
700
+ f.write(str(os.getpid()))
701
+
702
+ logger.info(f"Daemon started with PID {os.getpid()}")
703
+
704
+ # Set up signal handlers
705
+ signal.signal(signal.SIGTERM, self._signal_handler)
706
+ signal.signal(signal.SIGINT, self._signal_handler)
707
+
708
+ # Start main loop
709
+ try:
710
+ self._main_loop()
711
+ except KeyboardInterrupt:
712
+ logger.info("Daemon interrupted")
713
+ finally:
714
+ self.stop()
715
+
716
+ def stop(self):
717
+ """Stop the daemon service"""
718
+ if not self.running:
719
+ return
720
+
721
+ self.running = False
722
+
723
+ # Remove PID file
724
+ if self.pid_file.exists():
725
+ self.pid_file.unlink()
726
+
727
+ logger.info("Daemon stopped")
728
+
729
+ def _signal_handler(self, signum, frame):
730
+ """Handle shutdown signals"""
731
+ logger.info(f"Received signal {signum}, shutting down...")
732
+ self.stop()
733
+ sys.exit(0)
734
+
735
+ def _main_loop(self):
736
+ """Main daemon loop"""
737
+ logger.info("Daemon main loop started")
738
+
739
+ while self.running:
740
+ try:
741
+ # Check for commands to execute
742
+ # This is a simple implementation - in a real system you'd use
743
+ # a message queue or socket communication
744
+ time.sleep(1)
745
+
746
+ except Exception as e:
747
+ logger.error(f"Error in main loop: {e}")
748
+ time.sleep(5)
749
+
750
+ def status(self) -> Dict[str, Any]:
751
+ """Get daemon status"""
752
+ is_running = False
753
+ pid = None
754
+
755
+ if self.pid_file.exists():
756
+ try:
757
+ with open(self.pid_file, "r") as f:
758
+ pid = int(f.read().strip())
759
+ is_running = psutil.pid_exists(pid)
760
+ except Exception:
761
+ pass
762
+
763
+ return {
764
+ "running": is_running,
765
+ "pid": pid,
766
+ "pid_file": str(self.pid_file),
767
+ "socket_file": str(self.socket_file),
768
+ }
769
+
770
+
771
+ # CLI Commands
772
+ @click.group(name="daemon")
773
+ def daemon():
774
+ """Daemon service for command management"""
775
+ pass
776
+
777
+
778
+ @daemon.command()
779
+ @click.option("--config", help="Path to configuration file")
780
+ def start(config: Optional[str]):
781
+ """Start the daemon service"""
782
+ service = DaemonService(config)
783
+ service.start()
784
+
785
+
786
+ @daemon.command()
787
+ def stop():
788
+ """Stop the daemon service"""
789
+ pid_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.pid"
790
+
791
+ if not pid_file.exists():
792
+ click.echo("Daemon is not running")
793
+ return
794
+
795
+ try:
796
+ with open(pid_file, "r") as f:
797
+ pid = int(f.read().strip())
798
+
799
+ # Send SIGTERM
800
+ os.kill(pid, signal.SIGTERM)
801
+ click.echo(f"Sent stop signal to daemon (PID {pid})")
802
+
803
+ # Wait a bit and check if it stopped
804
+ time.sleep(2)
805
+ if not psutil.pid_exists(pid):
806
+ click.echo("Daemon stopped successfully")
807
+ else:
808
+ click.echo("Daemon may still be running")
809
+
810
+ except Exception as e:
811
+ click.echo(f"Error stopping daemon: {e}")
812
+
813
+
814
+ @daemon.command()
815
+ def status():
816
+ """Show daemon status"""
817
+ service = DaemonService()
818
+ status_info = service.status()
819
+ if status_info["running"]:
820
+ click.echo(f"✅ Daemon is running (PID: {status_info['pid']})")
821
+ else:
822
+ click.echo("❌ Daemon is not running")
823
+ click.echo(f"PID file: {status_info['pid_file']}")
824
+ click.echo(f"Socket file: {status_info['socket_file']}")
825
+
826
+
827
+ # --- New CLI: list-commands ---
828
+ @daemon.command("list-commands")
829
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
830
+ @click.option("--all", "show_all", is_flag=True, help="Show all commands, including inactive")
831
+ def list_commands(as_json, show_all):
832
+ """List all available commands (optionally including inactive)"""
833
+ import sys
834
+
835
+ service = DaemonService()
836
+ commands = service.db.get_all_commands(include_inactive=show_all)
837
+ result = []
838
+ for cmd in commands:
839
+ result.append(
840
+ {
841
+ "id": cmd.id,
842
+ "name": cmd.name,
843
+ "description": cmd.description,
844
+ "language": cmd.language,
845
+ "group": cmd.group,
846
+ "tags": cmd.tags,
847
+ "created_at": cmd.created_at.isoformat() if cmd.created_at else None,
848
+ "updated_at": cmd.updated_at.isoformat() if cmd.updated_at else None,
849
+ "execution_count": cmd.execution_count,
850
+ "last_executed": cmd.last_executed.isoformat() if cmd.last_executed else None,
851
+ "is_active": cmd.is_active,
852
+ }
853
+ )
854
+ if as_json:
855
+ import json
856
+
857
+ print(json.dumps({"commands": result}, indent=2))
858
+ else:
859
+ for cmd in result:
860
+ status = "[INACTIVE] " if not cmd["is_active"] else ""
861
+ click.echo(f"{status}- {cmd['name']} ({cmd['language']}) : {cmd['description']}")
862
+
863
+
864
+ # --- New CLI: execute ---
865
+ @daemon.command("execute")
866
+ @click.argument("command_name")
867
+ @click.argument("args", nargs=-1)
868
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
869
+ def execute_command(command_name, args, as_json):
870
+ """Execute a command by name with optional arguments"""
871
+ import sys
872
+
873
+ service = DaemonService()
874
+ # Find command by name
875
+ commands = service.db.get_all_commands()
876
+ cmd = next((c for c in commands if c.name == command_name), None)
877
+ if not cmd:
878
+ msg = f"Command '{command_name}' not found."
879
+ if as_json:
880
+ import json
881
+
882
+ print(json.dumps({"success": False, "error": msg}))
883
+ else:
884
+ click.echo(msg)
885
+ return
886
+ result = service.executor.execute_command(cmd, list(args))
887
+ if as_json:
888
+ import json
889
+
890
+ print(json.dumps(result, indent=2))
891
+ else:
892
+ if result.get("success"):
893
+ click.echo(result.get("output", ""))
894
+ else:
895
+ click.echo(f"Error: {result.get('error', '')}")
896
+
897
+
898
+ # Client commands - these will be moved to the client module
899
+ # but we'll keep the core daemon service commands here
900
+
901
+ if __name__ == "__main__":
902
+ # Start file watcher for hot-reloading file-based commands
903
+ db = CommandDatabase()
904
+ start_command_file_watcher(db)
905
+ daemon()