mcli-framework 7.1.3__py3-none-any.whl → 7.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/__init__.py +160 -0
- mcli/__main__.py +14 -0
- mcli/app/__init__.py +23 -0
- mcli/app/main.py +10 -0
- mcli/app/model/__init__.py +0 -0
- mcli/app/video/__init__.py +5 -0
- mcli/chat/__init__.py +34 -0
- mcli/lib/__init__.py +0 -0
- mcli/lib/api/__init__.py +0 -0
- mcli/lib/auth/__init__.py +1 -0
- mcli/lib/config/__init__.py +1 -0
- mcli/lib/custom_commands.py +424 -0
- mcli/lib/erd/__init__.py +25 -0
- mcli/lib/files/__init__.py +0 -0
- mcli/lib/fs/__init__.py +1 -0
- mcli/lib/logger/__init__.py +3 -0
- mcli/lib/paths.py +12 -0
- mcli/lib/performance/__init__.py +17 -0
- mcli/lib/pickles/__init__.py +1 -0
- mcli/lib/shell/__init__.py +0 -0
- mcli/lib/toml/__init__.py +1 -0
- mcli/lib/watcher/__init__.py +0 -0
- mcli/ml/__init__.py +16 -0
- mcli/ml/api/__init__.py +30 -0
- mcli/ml/api/routers/__init__.py +27 -0
- mcli/ml/api/schemas.py +2 -2
- mcli/ml/auth/__init__.py +45 -0
- mcli/ml/auth/models.py +2 -2
- mcli/ml/backtesting/__init__.py +39 -0
- mcli/ml/cli/__init__.py +5 -0
- mcli/ml/cli/main.py +1 -1
- mcli/ml/config/__init__.py +33 -0
- mcli/ml/configs/__init__.py +16 -0
- mcli/ml/dashboard/__init__.py +12 -0
- mcli/ml/dashboard/app.py +13 -13
- mcli/ml/dashboard/app_integrated.py +1309 -148
- mcli/ml/dashboard/app_supabase.py +46 -21
- mcli/ml/dashboard/app_training.py +14 -14
- mcli/ml/dashboard/components/__init__.py +7 -0
- mcli/ml/dashboard/components/charts.py +258 -0
- mcli/ml/dashboard/components/metrics.py +125 -0
- mcli/ml/dashboard/components/tables.py +228 -0
- mcli/ml/dashboard/pages/__init__.py +6 -0
- mcli/ml/dashboard/pages/cicd.py +382 -0
- mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
- mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
- mcli/ml/dashboard/pages/test_portfolio.py +373 -0
- mcli/ml/dashboard/pages/trading.py +714 -0
- mcli/ml/dashboard/pages/workflows.py +533 -0
- mcli/ml/dashboard/utils.py +154 -0
- mcli/ml/data_ingestion/__init__.py +39 -0
- mcli/ml/database/__init__.py +47 -0
- mcli/ml/experimentation/__init__.py +29 -0
- mcli/ml/features/__init__.py +39 -0
- mcli/ml/mlops/__init__.py +33 -0
- mcli/ml/models/__init__.py +94 -0
- mcli/ml/monitoring/__init__.py +25 -0
- mcli/ml/optimization/__init__.py +27 -0
- mcli/ml/predictions/__init__.py +5 -0
- mcli/ml/preprocessing/__init__.py +28 -0
- mcli/ml/scripts/__init__.py +1 -0
- mcli/ml/trading/__init__.py +60 -0
- mcli/ml/trading/alpaca_client.py +353 -0
- mcli/ml/trading/migrations.py +164 -0
- mcli/ml/trading/models.py +418 -0
- mcli/ml/trading/paper_trading.py +326 -0
- mcli/ml/trading/risk_management.py +370 -0
- mcli/ml/trading/trading_service.py +480 -0
- mcli/ml/training/__init__.py +10 -0
- mcli/ml/training/train_model.py +569 -0
- mcli/mygroup/__init__.py +3 -0
- mcli/public/__init__.py +1 -0
- mcli/public/commands/__init__.py +2 -0
- mcli/self/__init__.py +3 -0
- mcli/self/self_cmd.py +579 -91
- mcli/workflow/__init__.py +0 -0
- mcli/workflow/daemon/__init__.py +15 -0
- mcli/workflow/daemon/daemon.py +21 -3
- mcli/workflow/dashboard/__init__.py +5 -0
- mcli/workflow/docker/__init__.py +0 -0
- mcli/workflow/file/__init__.py +0 -0
- mcli/workflow/gcloud/__init__.py +1 -0
- mcli/workflow/git_commit/__init__.py +0 -0
- mcli/workflow/interview/__init__.py +0 -0
- mcli/workflow/politician_trading/__init__.py +4 -0
- mcli/workflow/politician_trading/data_sources.py +259 -1
- mcli/workflow/politician_trading/models.py +159 -1
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
- mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
- mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
- mcli/workflow/politician_trading/seed_database.py +539 -0
- mcli/workflow/registry/__init__.py +0 -0
- mcli/workflow/repo/__init__.py +0 -0
- mcli/workflow/scheduler/__init__.py +25 -0
- mcli/workflow/search/__init__.py +0 -0
- mcli/workflow/sync/__init__.py +5 -0
- mcli/workflow/videos/__init__.py +1 -0
- mcli/workflow/wakatime/__init__.py +80 -0
- mcli/workflow/workflow.py +8 -27
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
- mcli/workflow/daemon/api_daemon.py +0 -800
- mcli/workflow/daemon/commands.py +0 -1196
- mcli/workflow/dashboard/dashboard_cmd.py +0 -120
- mcli/workflow/file/file.py +0 -100
- mcli/workflow/git_commit/commands.py +0 -430
- mcli/workflow/politician_trading/commands.py +0 -1939
- mcli/workflow/scheduler/commands.py +0 -493
- mcli/workflow/sync/sync_cmd.py +0 -437
- mcli/workflow/videos/videos.py +0 -242
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
mcli/workflow/daemon/commands.py
DELETED
|
@@ -1,1196 +0,0 @@
|
|
|
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()
|