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,1196 @@
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import pickle
6
+ import shutil
7
+ import signal
8
+ import sqlite3
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import threading
13
+ import time
14
+ import uuid
15
+ from dataclasses import asdict, dataclass
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Union
19
+
20
+ import click
21
+ import psutil
22
+ from sklearn.feature_extraction.text import TfidfVectorizer
23
+ from sklearn.metrics.pairwise import cosine_similarity
24
+
25
+ # Import existing utilities
26
+ from mcli.lib.logger.logger import get_logger
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ @dataclass
32
+ class Command:
33
+ """Represents a stored command"""
34
+
35
+ id: str
36
+ name: str
37
+ description: str
38
+ code: str
39
+ language: str # 'python', 'node', 'lua', 'shell'
40
+ group: Optional[str] = None
41
+ tags: List[str] = None
42
+ created_at: datetime = None
43
+ updated_at: datetime = None
44
+ execution_count: int = 0
45
+ last_executed: Optional[datetime] = None
46
+ is_active: bool = True
47
+
48
+ def __post_init__(self):
49
+ if self.tags is None:
50
+ self.tags = []
51
+ if self.created_at is None:
52
+ self.created_at = datetime.now()
53
+ if self.updated_at is None:
54
+ self.updated_at = datetime.now()
55
+
56
+
57
+ class CommandDatabase:
58
+ """Manages command storage and retrieval"""
59
+
60
+ def __init__(self, db_path: Optional[str] = None):
61
+ if db_path is None:
62
+ db_path = Path.home() / ".local" / "mcli" / "daemon" / "commands.db"
63
+
64
+ self.db_path = Path(db_path)
65
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
66
+ self.init_database()
67
+
68
+ # Initialize vectorizer for similarity search
69
+ self.vectorizer = TfidfVectorizer(
70
+ max_features=1000, stop_words="english", ngram_range=(1, 2)
71
+ )
72
+ self._update_embeddings()
73
+
74
+ def init_database(self):
75
+ """Initialize SQLite database"""
76
+ conn = sqlite3.connect(self.db_path)
77
+ cursor = conn.cursor()
78
+
79
+ # Commands table
80
+ cursor.execute(
81
+ """
82
+ CREATE TABLE IF NOT EXISTS commands (
83
+ id TEXT PRIMARY KEY,
84
+ name TEXT NOT NULL,
85
+ description TEXT,
86
+ code TEXT NOT NULL,
87
+ language TEXT NOT NULL,
88
+ group_name TEXT,
89
+ tags TEXT,
90
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
91
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
+ execution_count INTEGER DEFAULT 0,
93
+ last_executed TIMESTAMP,
94
+ is_active BOOLEAN DEFAULT 1
95
+ )
96
+ """
97
+ )
98
+
99
+ # Groups table for hierarchical organization
100
+ cursor.execute(
101
+ """
102
+ CREATE TABLE IF NOT EXISTS groups (
103
+ id TEXT PRIMARY KEY,
104
+ name TEXT NOT NULL,
105
+ description TEXT,
106
+ parent_group_id TEXT,
107
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
108
+ FOREIGN KEY (parent_group_id) REFERENCES groups (id)
109
+ )
110
+ """
111
+ )
112
+
113
+ # Execution history
114
+ cursor.execute(
115
+ """
116
+ CREATE TABLE IF NOT EXISTS executions (
117
+ id TEXT PRIMARY KEY,
118
+ command_id TEXT NOT NULL,
119
+ executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
120
+ status TEXT NOT NULL,
121
+ output TEXT,
122
+ error TEXT,
123
+ execution_time_ms INTEGER,
124
+ FOREIGN KEY (command_id) REFERENCES commands (id)
125
+ )
126
+ """
127
+ )
128
+
129
+ conn.commit()
130
+ conn.close()
131
+
132
+ def _update_embeddings(self):
133
+ """Update TF-IDF embeddings for similarity search"""
134
+ commands = self.get_all_commands()
135
+ if not commands:
136
+ return
137
+
138
+ # Combine name, description, and tags for embedding
139
+ texts = []
140
+ for cmd in commands:
141
+ text_parts = [cmd.name, cmd.description or ""]
142
+ text_parts.extend(cmd.tags or [])
143
+ texts.append(" ".join(text_parts))
144
+
145
+ if texts:
146
+ self.vectorizer.fit(texts)
147
+
148
+ def add_command(self, command: Command) -> str:
149
+ """Add a new command to the database"""
150
+ conn = sqlite3.connect(self.db_path)
151
+ cursor = conn.cursor()
152
+
153
+ try:
154
+ cursor.execute(
155
+ """
156
+ INSERT INTO commands
157
+ (id, name, description, code, language, group_name, tags,
158
+ created_at, updated_at, execution_count, last_executed, is_active)
159
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
160
+ """,
161
+ (
162
+ command.id,
163
+ command.name,
164
+ command.description,
165
+ command.code,
166
+ command.language,
167
+ command.group,
168
+ json.dumps(command.tags),
169
+ command.created_at.isoformat(),
170
+ command.updated_at.isoformat(),
171
+ command.execution_count,
172
+ command.last_executed.isoformat() if command.last_executed else None,
173
+ command.is_active,
174
+ ),
175
+ )
176
+
177
+ conn.commit()
178
+ self._update_embeddings()
179
+ return command.id
180
+
181
+ except Exception as e:
182
+ logger.error(f"Error adding command: {e}")
183
+ conn.rollback()
184
+ raise
185
+ finally:
186
+ conn.close()
187
+
188
+ def get_command(self, command_id: str) -> Optional[Command]:
189
+ """Get a command by ID"""
190
+ conn = sqlite3.connect(self.db_path)
191
+ cursor = conn.cursor()
192
+
193
+ try:
194
+ cursor.execute(
195
+ """
196
+ SELECT id, name, description, code, language, group_name, tags,
197
+ created_at, updated_at, execution_count, last_executed, is_active
198
+ FROM commands WHERE id = ?
199
+ """,
200
+ (command_id,),
201
+ )
202
+
203
+ row = cursor.fetchone()
204
+ if row:
205
+ return self._row_to_command(row)
206
+ return None
207
+
208
+ finally:
209
+ conn.close()
210
+
211
+ def get_all_commands(self, include_inactive: bool = False) -> List[Command]:
212
+ """Get all commands with optional inactive inclusion"""
213
+ conn = sqlite3.connect(self.db_path)
214
+ cursor = conn.cursor()
215
+
216
+ try:
217
+ query = """
218
+ SELECT id, name, description, code, language, group_name, tags,
219
+ created_at, updated_at, execution_count, last_executed, is_active
220
+ FROM commands
221
+ """
222
+ if not include_inactive:
223
+ query += " WHERE is_active = 1"
224
+ query += " ORDER BY name"
225
+
226
+ cursor.execute(query)
227
+
228
+ return [self._row_to_command(row) for row in cursor.fetchall()]
229
+
230
+ finally:
231
+ conn.close()
232
+
233
+ def search_commands(self, query: str, limit: int = 10) -> List[Command]:
234
+ """Search commands by name, description, or tags"""
235
+ conn = sqlite3.connect(self.db_path)
236
+ cursor = conn.cursor()
237
+
238
+ try:
239
+ # Simple text search
240
+ search_term = f"%{query}%"
241
+ cursor.execute(
242
+ """
243
+ SELECT id, name, description, code, language, group_name, tags,
244
+ created_at, updated_at, execution_count, last_executed, is_active
245
+ FROM commands
246
+ WHERE is_active = 1
247
+ AND (name LIKE ? OR description LIKE ? OR tags LIKE ? OR language LIKE ?)
248
+ ORDER BY name
249
+ LIMIT ?
250
+ """,
251
+ (search_term, search_term, search_term, search_term, limit),
252
+ )
253
+
254
+ return [self._row_to_command(row) for row in cursor.fetchall()]
255
+
256
+ finally:
257
+ conn.close()
258
+
259
+ def find_similar_commands(self, query: str, limit: int = 5) -> List[tuple]:
260
+ """Find similar commands using cosine similarity"""
261
+ cmd_list = self.get_all_commands()
262
+ if not cmd_list:
263
+ return []
264
+
265
+ # Prepare query text
266
+ query_text = query.lower()
267
+
268
+ # Get command texts for comparison
269
+ command_texts = []
270
+ for cmd in cmd_list:
271
+ text_parts = [cmd.name, cmd.description or ""]
272
+ text_parts.extend(cmd.tags or [])
273
+ command_texts.append(" ".join(text_parts).lower())
274
+
275
+ if not command_texts:
276
+ return []
277
+
278
+ # Calculate similarities
279
+ try:
280
+ # Re-fit vectorizer with current commands if needed
281
+ if len(command_texts) > 0:
282
+ # Create a temporary vectorizer for this search
283
+ temp_vectorizer = TfidfVectorizer(
284
+ max_features=1000, stop_words="english", ngram_range=(1, 2)
285
+ )
286
+
287
+ # Fit on current command texts
288
+ all_texts = command_texts + [query_text]
289
+ temp_vectorizer.fit(all_texts)
290
+
291
+ # Transform query and commands
292
+ query_vector = temp_vectorizer.transform([query_text])
293
+ command_vectors = temp_vectorizer.transform(command_texts)
294
+
295
+ # Calculate cosine similarities
296
+ similarities = cosine_similarity(query_vector, command_vectors).flatten()
297
+
298
+ # Sort by similarity - avoid using 'commands' variable name
299
+ cmd_similarities = []
300
+ for i, similarity_score in enumerate(similarities):
301
+ cmd_similarities.append((cmd_list[i], similarity_score))
302
+
303
+ cmd_similarities.sort(key=lambda x: x[1], reverse=True)
304
+
305
+ return cmd_similarities[:limit]
306
+ else:
307
+ return []
308
+
309
+ except Exception as e:
310
+ logger.error(f"Error calculating similarities: {e}")
311
+ import traceback
312
+
313
+ traceback.print_exc()
314
+ return []
315
+
316
+ def update_command(self, command: Command) -> bool:
317
+ """Update an existing command"""
318
+ conn = sqlite3.connect(self.db_path)
319
+ cursor = conn.cursor()
320
+
321
+ try:
322
+ cursor.execute(
323
+ """
324
+ UPDATE commands
325
+ SET name = ?, description = ?, code = ?, language = ?,
326
+ group_name = ?, tags = ?, updated_at = ?, is_active = ?
327
+ WHERE id = ?
328
+ """,
329
+ (
330
+ command.name,
331
+ command.description,
332
+ command.code,
333
+ command.language,
334
+ command.group,
335
+ json.dumps(command.tags),
336
+ datetime.now().isoformat(),
337
+ command.is_active,
338
+ command.id,
339
+ ),
340
+ )
341
+
342
+ conn.commit()
343
+ self._update_embeddings()
344
+ return cursor.rowcount > 0
345
+
346
+ except Exception as e:
347
+ logger.error(f"Error updating command: {e}")
348
+ conn.rollback()
349
+ return False
350
+ finally:
351
+ conn.close()
352
+
353
+ def delete_command(self, command_id: str) -> bool:
354
+ """Delete a command (soft delete)"""
355
+ conn = sqlite3.connect(self.db_path)
356
+ cursor = conn.cursor()
357
+
358
+ try:
359
+ cursor.execute(
360
+ """
361
+ UPDATE commands SET is_active = 0 WHERE id = ?
362
+ """,
363
+ (command_id,),
364
+ )
365
+
366
+ conn.commit()
367
+ self._update_embeddings()
368
+ return cursor.rowcount > 0
369
+
370
+ except Exception as e:
371
+ logger.error(f"Error deleting command: {e}")
372
+ conn.rollback()
373
+ return False
374
+ finally:
375
+ conn.close()
376
+
377
+ def record_execution(
378
+ self,
379
+ command_id: str,
380
+ status: str,
381
+ output: str = None,
382
+ error: str = None,
383
+ execution_time_ms: int = None,
384
+ ):
385
+ """Record command execution"""
386
+ conn = sqlite3.connect(self.db_path)
387
+ cursor = conn.cursor()
388
+
389
+ try:
390
+ # Record execution
391
+ execution_id = str(uuid.uuid4())
392
+ cursor.execute(
393
+ """
394
+ INSERT INTO executions
395
+ (id, command_id, executed_at, status, output, error, execution_time_ms)
396
+ VALUES (?, ?, ?, ?, ?, ?, ?)
397
+ """,
398
+ (
399
+ execution_id,
400
+ command_id,
401
+ datetime.now().isoformat(),
402
+ status,
403
+ output,
404
+ error,
405
+ execution_time_ms,
406
+ ),
407
+ )
408
+
409
+ # Update command stats
410
+ cursor.execute(
411
+ """
412
+ UPDATE commands
413
+ SET execution_count = execution_count + 1,
414
+ last_executed = ?
415
+ WHERE id = ?
416
+ """,
417
+ (datetime.now().isoformat(), command_id),
418
+ )
419
+
420
+ conn.commit()
421
+
422
+ except Exception as e:
423
+ logger.error(f"Error recording execution: {e}")
424
+ conn.rollback()
425
+ finally:
426
+ conn.close()
427
+
428
+ def _row_to_command(self, row) -> Command:
429
+ """Convert database row to Command object"""
430
+ return Command(
431
+ id=row[0],
432
+ name=row[1],
433
+ description=row[2],
434
+ code=row[3],
435
+ language=row[4],
436
+ group=row[5],
437
+ tags=json.loads(row[6]) if row[6] else [],
438
+ created_at=datetime.fromisoformat(row[7]),
439
+ updated_at=datetime.fromisoformat(row[8]),
440
+ execution_count=row[9],
441
+ last_executed=datetime.fromisoformat(row[10]) if row[10] else None,
442
+ is_active=bool(row[11]),
443
+ )
444
+
445
+
446
+ class CommandExecutor:
447
+ """Handles safe execution of commands in different languages"""
448
+
449
+ def __init__(self, temp_dir: Optional[str] = None):
450
+ self.temp_dir = Path(temp_dir) if temp_dir else Path(tempfile.gettempdir()) / "mcli_daemon"
451
+ self.temp_dir.mkdir(parents=True, exist_ok=True)
452
+
453
+ # Language-specific execution environments
454
+ self.language_handlers = {
455
+ "python": self._execute_python,
456
+ "node": self._execute_node,
457
+ "lua": self._execute_lua,
458
+ "shell": self._execute_shell,
459
+ }
460
+
461
+ def execute_command(self, command: Command, args: List[str] = None) -> Dict[str, Any]:
462
+ """Execute a command safely"""
463
+ start_time = time.time()
464
+
465
+ try:
466
+ # Get the appropriate handler
467
+ handler = self.language_handlers.get(command.language)
468
+ if not handler:
469
+ raise ValueError(f"Unsupported language: {command.language}")
470
+
471
+ # Execute the command
472
+ result = handler(command, args or [])
473
+
474
+ execution_time = int((time.time() - start_time) * 1000)
475
+
476
+ # Check if execution was successful
477
+ returncode = result.get("returncode", 0)
478
+ success = returncode == 0
479
+ status = "completed" if success else "failed"
480
+
481
+ return {
482
+ "success": success,
483
+ "output": result.get("output", ""),
484
+ "error": result.get("error", ""),
485
+ "execution_time_ms": execution_time,
486
+ "status": status,
487
+ }
488
+
489
+ except Exception as e:
490
+ execution_time = int((time.time() - start_time) * 1000)
491
+ return {
492
+ "success": False,
493
+ "output": "",
494
+ "error": str(e),
495
+ "execution_time_ms": execution_time,
496
+ "status": "failed",
497
+ }
498
+
499
+ def _execute_python(self, command: Command, args: List[str]) -> Dict[str, str]:
500
+ """Execute Python code safely with resource limits and sandboxing"""
501
+ # Create secure temporary file
502
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.py"
503
+ script_file.touch(mode=0o700) # Restrict permissions
504
+
505
+ # Add resource limits
506
+ resource_limits = """
507
+ import resource
508
+ resource.setrlimit(resource.RLIMIT_CPU, (1, 1)) # 1 second CPU time
509
+ resource.setrlimit(resource.RLIMIT_AS, (256*1024*1024, 256*1024*1024)) # 256MB memory
510
+ """
511
+
512
+ try:
513
+ # Write code to file
514
+ with open(script_file, "w") as f:
515
+ f.write(command.code)
516
+
517
+ # Execute with subprocess
518
+ result = subprocess.run(
519
+ [sys.executable, str(script_file)] + args,
520
+ capture_output=True,
521
+ text=True,
522
+ timeout=30, # 30 second timeout
523
+ cwd=self.temp_dir,
524
+ )
525
+
526
+ return {
527
+ "output": result.stdout,
528
+ "error": result.stderr,
529
+ "returncode": result.returncode,
530
+ }
531
+
532
+ finally:
533
+ # Clean up
534
+ if script_file.exists():
535
+ script_file.unlink()
536
+
537
+ def _execute_node(self, command: Command, args: List[str]) -> Dict[str, str]:
538
+ """Execute Node.js code safely"""
539
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.js"
540
+
541
+ try:
542
+ with open(script_file, "w") as f:
543
+ f.write(command.code)
544
+
545
+ result = subprocess.run(
546
+ ["node", str(script_file)] + args,
547
+ capture_output=True,
548
+ text=True,
549
+ timeout=30,
550
+ cwd=self.temp_dir,
551
+ )
552
+
553
+ return {
554
+ "output": result.stdout,
555
+ "error": result.stderr,
556
+ "returncode": result.returncode,
557
+ }
558
+
559
+ finally:
560
+ if script_file.exists():
561
+ script_file.unlink()
562
+
563
+ def _execute_lua(self, command: Command, args: List[str]) -> Dict[str, str]:
564
+ """Execute Lua code safely"""
565
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.lua"
566
+
567
+ try:
568
+ with open(script_file, "w") as f:
569
+ f.write(command.code)
570
+
571
+ result = subprocess.run(
572
+ ["lua", str(script_file)] + args,
573
+ capture_output=True,
574
+ text=True,
575
+ timeout=30,
576
+ cwd=self.temp_dir,
577
+ )
578
+
579
+ return {
580
+ "output": result.stdout,
581
+ "error": result.stderr,
582
+ "returncode": result.returncode,
583
+ }
584
+
585
+ finally:
586
+ if script_file.exists():
587
+ script_file.unlink()
588
+
589
+ def _execute_shell(self, command: Command, args: List[str]) -> Dict[str, str]:
590
+ """Execute shell commands safely"""
591
+ script_file = self.temp_dir / f"{command.id}_{int(time.time())}.sh"
592
+
593
+ try:
594
+ with open(script_file, "w") as f:
595
+ f.write("#!/bin/bash\n")
596
+ f.write(command.code)
597
+
598
+ # Make executable
599
+ script_file.chmod(0o755)
600
+
601
+ result = subprocess.run(
602
+ [str(script_file)] + args,
603
+ capture_output=True,
604
+ text=True,
605
+ timeout=30,
606
+ cwd=self.temp_dir,
607
+ )
608
+
609
+ return {
610
+ "output": result.stdout,
611
+ "error": result.stderr,
612
+ "returncode": result.returncode,
613
+ }
614
+
615
+ finally:
616
+ if script_file.exists():
617
+ script_file.unlink()
618
+
619
+
620
+ class DaemonService:
621
+ """Background daemon service for command management"""
622
+
623
+ def __init__(self, config_path: Optional[str] = None):
624
+ self.db = CommandDatabase()
625
+ self.executor = CommandExecutor()
626
+ self.running = False
627
+ self.pid_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.pid"
628
+ self.socket_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.sock"
629
+
630
+ # Ensure daemon directory exists
631
+ self.pid_file.parent.mkdir(parents=True, exist_ok=True)
632
+
633
+ def start(self):
634
+ """Start the daemon service"""
635
+ if self.running:
636
+ logger.info("Daemon is already running")
637
+ return
638
+
639
+ # Check if already running
640
+ if self.pid_file.exists():
641
+ try:
642
+ with open(self.pid_file, "r") as f:
643
+ pid = int(f.read().strip())
644
+ if psutil.pid_exists(pid):
645
+ logger.info(f"Daemon already running with PID {pid}")
646
+ return
647
+ except Exception:
648
+ pass
649
+
650
+ # Start daemon
651
+ self.running = True
652
+
653
+ # Write PID file
654
+ with open(self.pid_file, "w") as f:
655
+ f.write(str(os.getpid()))
656
+
657
+ logger.info(f"Daemon started with PID {os.getpid()}")
658
+
659
+ # Set up signal handlers
660
+ signal.signal(signal.SIGTERM, self._signal_handler)
661
+ signal.signal(signal.SIGINT, self._signal_handler)
662
+
663
+ # Start main loop
664
+ try:
665
+ self._main_loop()
666
+ except KeyboardInterrupt:
667
+ logger.info("Daemon interrupted")
668
+ finally:
669
+ self.stop()
670
+
671
+ def stop(self):
672
+ """Stop the daemon service"""
673
+ if not self.running:
674
+ return
675
+
676
+ self.running = False
677
+
678
+ # Remove PID file
679
+ if self.pid_file.exists():
680
+ self.pid_file.unlink()
681
+
682
+ logger.info("Daemon stopped")
683
+
684
+ def _signal_handler(self, signum, frame):
685
+ """Handle shutdown signals"""
686
+ logger.info(f"Received signal {signum}, shutting down...")
687
+ self.stop()
688
+ sys.exit(0)
689
+
690
+ def _main_loop(self):
691
+ """Main daemon loop"""
692
+ logger.info("Daemon main loop started")
693
+
694
+ while self.running:
695
+ try:
696
+ # Check for commands to execute
697
+ # This is a simple implementation - in a real system you'd use
698
+ # a message queue or socket communication
699
+ time.sleep(1)
700
+
701
+ except Exception as e:
702
+ logger.error(f"Error in main loop: {e}")
703
+ time.sleep(5)
704
+
705
+ def status(self) -> Dict[str, Any]:
706
+ """Get daemon status"""
707
+ is_running = False
708
+ pid = None
709
+
710
+ if self.pid_file.exists():
711
+ try:
712
+ with open(self.pid_file, "r") as f:
713
+ pid = int(f.read().strip())
714
+ is_running = psutil.pid_exists(pid)
715
+ except Exception:
716
+ pass
717
+
718
+ return {
719
+ "running": is_running,
720
+ "pid": pid,
721
+ "pid_file": str(self.pid_file),
722
+ "socket_file": str(self.socket_file),
723
+ }
724
+
725
+
726
+ # Create the daemon command group
727
+ @click.group(name="daemon")
728
+ def daemon():
729
+ """Daemon service for command management"""
730
+ pass
731
+
732
+
733
+ # Daemon service commands
734
+ @daemon.command()
735
+ @click.option("--config", help="Path to configuration file")
736
+ def start(config: Optional[str]):
737
+ """Start the daemon service"""
738
+ service = DaemonService(config)
739
+ service.start()
740
+
741
+
742
+ @daemon.command()
743
+ def stop():
744
+ """Stop the daemon service"""
745
+ pid_file = Path.home() / ".local" / "mcli" / "daemon" / "daemon.pid"
746
+
747
+ if not pid_file.exists():
748
+ click.echo("Daemon is not running")
749
+ return
750
+
751
+ try:
752
+ with open(pid_file, "r") as f:
753
+ pid = int(f.read().strip())
754
+
755
+ # Send SIGTERM
756
+ os.kill(pid, signal.SIGTERM)
757
+ click.echo(f"Sent stop signal to daemon (PID {pid})")
758
+
759
+ # Wait a bit and check if it stopped
760
+ time.sleep(2)
761
+ if not psutil.pid_exists(pid):
762
+ click.echo("Daemon stopped successfully")
763
+ else:
764
+ click.echo("Daemon may still be running")
765
+
766
+ except Exception as e:
767
+ click.echo(f"Error stopping daemon: {e}")
768
+
769
+
770
+ @daemon.command()
771
+ def status():
772
+ """Show daemon status"""
773
+ service = DaemonService()
774
+ status_info = service.status()
775
+
776
+ if status_info["running"]:
777
+ click.echo(f"✅ Daemon is running (PID: {status_info['pid']})")
778
+ else:
779
+ click.echo("❌ Daemon is not running")
780
+
781
+ click.echo(f"PID file: {status_info['pid_file']}")
782
+ click.echo(f"Socket file: {status_info['socket_file']}")
783
+
784
+
785
+ # Client commands
786
+ @daemon.command()
787
+ @click.argument("name")
788
+ @click.argument("file_path", type=click.Path(exists=True))
789
+ @click.option("--description", help="Command description")
790
+ @click.option(
791
+ "--language",
792
+ type=click.Choice(["python", "node", "lua", "shell", "auto"]),
793
+ default="auto",
794
+ help="Programming language",
795
+ )
796
+ @click.option("--group", help="Command group")
797
+ @click.option("--tags", help="Comma-separated tags")
798
+ def add_file(name: str, file_path: str, description: str, language: str, group: str, tags: str):
799
+ """Add a command from a file"""
800
+ db = CommandDatabase()
801
+
802
+ # Read code from file
803
+ with open(file_path, "r") as f:
804
+ code_content = f.read()
805
+
806
+ # Parse tags
807
+ tag_list = [tag.strip() for tag in tags.split(",")] if tags else []
808
+
809
+ # Create command
810
+ command = Command(
811
+ id=str(uuid.uuid4()),
812
+ name=name,
813
+ description=description or "",
814
+ code=code_content,
815
+ language=language,
816
+ group=group,
817
+ tags=tag_list,
818
+ )
819
+
820
+ # Add to database
821
+ command_id = db.add_command(command)
822
+ click.echo(f"✅ Command '{name}' added with ID: {command_id}")
823
+
824
+
825
+ @daemon.command()
826
+ @click.argument("name")
827
+ @click.option("--description", help="Command description")
828
+ @click.option(
829
+ "--language",
830
+ type=click.Choice(["python", "node", "lua", "shell"]),
831
+ default="python",
832
+ help="Programming language",
833
+ )
834
+ @click.option("--group", help="Command group")
835
+ @click.option("--tags", help="Comma-separated tags")
836
+ def add_stdin(name: str, description: str, language: str, group: str, tags: str):
837
+ """Add a command from stdin"""
838
+ db = CommandDatabase()
839
+
840
+ # Parse tags
841
+ tag_list = [tag.strip() for tag in tags.split(",")] if tags else []
842
+
843
+ click.echo("Enter your code (Ctrl+D when done):")
844
+
845
+ # Read from stdin
846
+ lines = []
847
+ try:
848
+ while True:
849
+ line = input()
850
+ lines.append(line)
851
+ except EOFError:
852
+ pass
853
+
854
+ code = "\n".join(lines)
855
+
856
+ if not code.strip():
857
+ click.echo("No code provided", err=True)
858
+ return
859
+
860
+ # Create command
861
+ command = Command(
862
+ id=str(uuid.uuid4()),
863
+ name=name,
864
+ description=description or "",
865
+ code=code,
866
+ language=language,
867
+ group=group,
868
+ tags=tag_list,
869
+ )
870
+
871
+ # Add to database
872
+ command_id = db.add_command(command)
873
+ click.echo(f"✅ Command '{name}' added with ID: {command_id}")
874
+
875
+
876
+ @daemon.command()
877
+ def add_interactive():
878
+ """Add a command interactively"""
879
+ db = CommandDatabase()
880
+
881
+ # Get command name
882
+ name = click.prompt("Command name", type=str)
883
+
884
+ # Check if name already exists
885
+ existing = db.search_commands(name, limit=1)
886
+ if existing and existing[0].name == name:
887
+ if not click.confirm(f"Command '{name}' already exists. Overwrite?"):
888
+ click.echo("Command creation cancelled")
889
+ return
890
+
891
+ # Get description
892
+ description = click.prompt("Description (optional)", type=str, default="")
893
+
894
+ # Get language
895
+ language = click.prompt(
896
+ "Language", type=click.Choice(["python", "node", "lua", "shell"]), default="python"
897
+ )
898
+
899
+ # Get group
900
+ group = click.prompt("Group (optional)", type=str, default="")
901
+ if not group:
902
+ group = None
903
+
904
+ # Get tags
905
+ tags_input = click.prompt("Tags (comma-separated, optional)", type=str, default="")
906
+ tags = [tag.strip() for tag in tags_input.split(",")] if tags_input else []
907
+
908
+ # Get code source
909
+ source = click.prompt(
910
+ "Code source", type=click.Choice(["file", "stdin", "paste"]), default="paste"
911
+ )
912
+
913
+ if source == "file":
914
+ file_path = click.prompt("File path", type=click.Path(exists=True))
915
+ with open(file_path, "r") as f:
916
+ code = f.read()
917
+ elif source == "stdin":
918
+ click.echo("Enter your code (Ctrl+D when done):")
919
+ lines = []
920
+ try:
921
+ while True:
922
+ line = input()
923
+ lines.append(line)
924
+ except EOFError:
925
+ pass
926
+ code = "\n".join(lines)
927
+ else: # paste
928
+ click.echo("Paste your code below (Ctrl+D when done):")
929
+ lines = []
930
+ try:
931
+ while True:
932
+ line = input()
933
+ lines.append(line)
934
+ except EOFError:
935
+ pass
936
+ code = "\n".join(lines)
937
+
938
+ if not code.strip():
939
+ click.echo("No code provided", err=True)
940
+ return
941
+
942
+ # Create command
943
+ command = Command(
944
+ id=str(uuid.uuid4()),
945
+ name=name,
946
+ description=description or "",
947
+ code=code,
948
+ language=language,
949
+ group=group,
950
+ tags=tags,
951
+ )
952
+
953
+ # Add to database
954
+ command_id = db.add_command(command)
955
+ click.echo(f"✅ Command '{name}' added with ID: {command_id}")
956
+
957
+
958
+ @daemon.command()
959
+ @click.argument("command_id")
960
+ @click.argument("args", nargs=-1)
961
+ def execute(command_id: str, args: List[str]):
962
+ """Execute a command"""
963
+ db = CommandDatabase()
964
+ executor = CommandExecutor()
965
+
966
+ # Get command
967
+ command = db.get_command(command_id)
968
+ if not command:
969
+ click.echo(f"Command '{command_id}' not found", err=True)
970
+ return
971
+
972
+ # Execute
973
+ result = executor.execute_command(command, list(args))
974
+
975
+ # Record execution
976
+ db.record_execution(
977
+ command_id=command_id,
978
+ status=result["status"],
979
+ output=result.get("output", ""),
980
+ error=result.get("error", ""),
981
+ execution_time_ms=result.get("execution_time_ms", 0),
982
+ )
983
+
984
+ # Display results
985
+ if result["success"]:
986
+ click.echo("✅ Command executed successfully")
987
+ if result["output"]:
988
+ click.echo("Output:")
989
+ click.echo(result["output"])
990
+ else:
991
+ click.echo("❌ Command execution failed")
992
+ if result["error"]:
993
+ click.echo(f"Error: {result['error']}")
994
+
995
+ click.echo(f"Execution time: {result.get('execution_time_ms', 0)}ms")
996
+
997
+
998
+ @daemon.command()
999
+ @click.argument("query")
1000
+ @click.option("--limit", default=10, help="Maximum number of results")
1001
+ @click.option("--similar", is_flag=True, help="Use similarity search")
1002
+ def search(query: str, limit: int, similar: bool):
1003
+ """Search for commands"""
1004
+ db = CommandDatabase()
1005
+
1006
+ try:
1007
+ if similar:
1008
+ results = db.find_similar_commands(query, limit)
1009
+ if results:
1010
+ click.echo(f"Found {len(results)} similar command(s):")
1011
+ for cmd, similarity in results:
1012
+ click.echo(f" {cmd.name} ({cmd.language}) - {cmd.description}")
1013
+ click.echo(f" Similarity: {similarity:.3f}")
1014
+ if cmd.tags:
1015
+ click.echo(f" Tags: {', '.join(cmd.tags)}")
1016
+ click.echo()
1017
+ else:
1018
+ click.echo("No similar commands found")
1019
+ else:
1020
+ commands = db.search_commands(query, limit)
1021
+ if commands:
1022
+ click.echo(f"Found {len(commands)} command(s):")
1023
+ for cmd in commands:
1024
+ click.echo(f" {cmd.name} ({cmd.language}) - {cmd.description}")
1025
+ if cmd.tags:
1026
+ click.echo(f" Tags: {', '.join(cmd.tags)}")
1027
+ click.echo()
1028
+ else:
1029
+ click.echo("No commands found")
1030
+
1031
+ except Exception as e:
1032
+ click.echo(f"❌ Error searching commands: {e}", err=True)
1033
+
1034
+
1035
+ @daemon.command()
1036
+ @click.option("--group", help="Filter by group")
1037
+ @click.option("--language", help="Filter by language")
1038
+ def list(group: str, language: str):
1039
+ """List all commands"""
1040
+ db = CommandDatabase()
1041
+
1042
+ try:
1043
+ commands = db.get_all_commands()
1044
+
1045
+ # Apply filters
1046
+ if group:
1047
+ commands = [cmd for cmd in commands if cmd.group == group]
1048
+ if language:
1049
+ commands = [cmd for cmd in commands if cmd.language == language]
1050
+
1051
+ if not commands:
1052
+ click.echo("No commands found")
1053
+ return
1054
+
1055
+ click.echo(f"Found {len(commands)} command(s):")
1056
+ for cmd in commands:
1057
+ click.echo(f" {cmd.name} ({cmd.language}) - {cmd.description}")
1058
+ if cmd.group:
1059
+ click.echo(f" Group: {cmd.group}")
1060
+ if cmd.tags:
1061
+ click.echo(f" Tags: {', '.join(cmd.tags)}")
1062
+ click.echo(f" Executed {cmd.execution_count} times")
1063
+ click.echo()
1064
+
1065
+ except Exception as e:
1066
+ click.echo(f"❌ Error listing commands: {e}", err=True)
1067
+
1068
+
1069
+ @daemon.command()
1070
+ @click.argument("command_id")
1071
+ def show(command_id: str):
1072
+ """Show command details"""
1073
+ db = CommandDatabase()
1074
+
1075
+ try:
1076
+ command = db.get_command(command_id)
1077
+ if not command:
1078
+ click.echo(f"Command '{command_id}' not found", err=True)
1079
+ return
1080
+
1081
+ click.echo(f"Command: {command.name}")
1082
+ click.echo(f"ID: {command.id}")
1083
+ click.echo(f"Description: {command.description}")
1084
+ click.echo(f"Language: {command.language}")
1085
+ if command.group:
1086
+ click.echo(f"Group: {command.group}")
1087
+ if command.tags:
1088
+ click.echo(f"Tags: {', '.join(command.tags)}")
1089
+ click.echo(f"Created: {command.created_at}")
1090
+ click.echo(f"Updated: {command.updated_at}")
1091
+ click.echo(f"Executed: {command.execution_count} times")
1092
+ if command.last_executed:
1093
+ click.echo(f"Last executed: {command.last_executed}")
1094
+ click.echo()
1095
+ click.echo("Code:")
1096
+ click.echo("=" * 50)
1097
+ click.echo(command.code)
1098
+ click.echo("=" * 50)
1099
+
1100
+ except Exception as e:
1101
+ click.echo(f"❌ Error showing command: {e}", err=True)
1102
+
1103
+
1104
+ @daemon.command()
1105
+ @click.argument("command_id")
1106
+ def delete(command_id: str):
1107
+ """Delete a command"""
1108
+ db = CommandDatabase()
1109
+
1110
+ try:
1111
+ command = db.get_command(command_id)
1112
+ if not command:
1113
+ click.echo(f"Command '{command_id}' not found", err=True)
1114
+ return
1115
+
1116
+ if click.confirm(f"Are you sure you want to delete command '{command.name}'?"):
1117
+ if db.delete_command(command_id):
1118
+ click.echo(f"✅ Command '{command.name}' deleted")
1119
+ else:
1120
+ click.echo(f"❌ Error deleting command '{command.name}'")
1121
+ else:
1122
+ click.echo("Deletion cancelled")
1123
+
1124
+ except Exception as e:
1125
+ click.echo(f"❌ Error deleting command: {e}", err=True)
1126
+
1127
+
1128
+ @daemon.command()
1129
+ @click.argument("command_id")
1130
+ @click.option("--name", help="New name")
1131
+ @click.option("--description", help="New description")
1132
+ @click.option("--group", help="New group")
1133
+ @click.option("--tags", help="New tags (comma-separated)")
1134
+ def edit(command_id: str, name: str, description: str, group: str, tags: str):
1135
+ """Edit a command"""
1136
+ db = CommandDatabase()
1137
+
1138
+ try:
1139
+ command = db.get_command(command_id)
1140
+ if not command:
1141
+ click.echo(f"Command '{command_id}' not found", err=True)
1142
+ return
1143
+
1144
+ # Update fields if provided
1145
+ if name:
1146
+ command.name = name
1147
+ if description:
1148
+ command.description = description
1149
+ if group:
1150
+ command.group = group
1151
+ if tags:
1152
+ command.tags = [tag.strip() for tag in tags.split(",")]
1153
+
1154
+ command.updated_at = datetime.now()
1155
+
1156
+ if db.update_command(command):
1157
+ click.echo(f"✅ Command '{command.name}' updated")
1158
+ else:
1159
+ click.echo(f"❌ Error updating command '{command.name}'")
1160
+
1161
+ except Exception as e:
1162
+ click.echo(f"❌ Error editing command: {e}", err=True)
1163
+
1164
+
1165
+ @daemon.command()
1166
+ def groups():
1167
+ """List all command groups"""
1168
+ db = CommandDatabase()
1169
+
1170
+ try:
1171
+ commands = db.get_all_commands()
1172
+ groups = {}
1173
+
1174
+ for cmd in commands:
1175
+ group = cmd.group or "ungrouped"
1176
+ if group not in groups:
1177
+ groups[group] = []
1178
+ groups[group].append(cmd)
1179
+
1180
+ if not groups:
1181
+ click.echo("No groups found")
1182
+ return
1183
+
1184
+ click.echo("Command groups:")
1185
+ for group_name, group_commands in groups.items():
1186
+ click.echo(f" {group_name} ({len(group_commands)} commands)")
1187
+ for cmd in group_commands:
1188
+ click.echo(f" - {cmd.name} ({cmd.language})")
1189
+ click.echo()
1190
+
1191
+ except Exception as e:
1192
+ click.echo(f"❌ Error listing groups: {e}", err=True)
1193
+
1194
+
1195
+ if __name__ == "__main__":
1196
+ daemon()