mcli-framework 7.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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()
|