mcp-proxy-adapter 4.1.1__py3-none-any.whl → 6.0.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.
- mcp_proxy_adapter/__main__.py +32 -0
- mcp_proxy_adapter/api/app.py +290 -33
- mcp_proxy_adapter/api/handlers.py +32 -6
- mcp_proxy_adapter/api/middleware/__init__.py +38 -32
- mcp_proxy_adapter/api/middleware/command_permission_middleware.py +148 -0
- mcp_proxy_adapter/api/middleware/error_handling.py +9 -0
- mcp_proxy_adapter/api/middleware/factory.py +243 -0
- mcp_proxy_adapter/api/middleware/logging.py +32 -6
- mcp_proxy_adapter/api/middleware/protocol_middleware.py +201 -0
- mcp_proxy_adapter/api/middleware/transport_middleware.py +122 -0
- mcp_proxy_adapter/api/middleware/unified_security.py +197 -0
- mcp_proxy_adapter/api/middleware/user_info_middleware.py +158 -0
- mcp_proxy_adapter/commands/__init__.py +19 -4
- mcp_proxy_adapter/commands/auth_validation_command.py +408 -0
- mcp_proxy_adapter/commands/base.py +66 -32
- mcp_proxy_adapter/commands/builtin_commands.py +95 -0
- mcp_proxy_adapter/commands/catalog_manager.py +838 -0
- mcp_proxy_adapter/commands/cert_monitor_command.py +620 -0
- mcp_proxy_adapter/commands/certificate_management_command.py +608 -0
- mcp_proxy_adapter/commands/command_registry.py +711 -354
- mcp_proxy_adapter/commands/dependency_manager.py +245 -0
- mcp_proxy_adapter/commands/echo_command.py +81 -0
- mcp_proxy_adapter/commands/health_command.py +8 -1
- mcp_proxy_adapter/commands/help_command.py +21 -14
- mcp_proxy_adapter/commands/hooks.py +200 -167
- mcp_proxy_adapter/commands/key_management_command.py +506 -0
- mcp_proxy_adapter/commands/load_command.py +176 -0
- mcp_proxy_adapter/commands/plugins_command.py +235 -0
- mcp_proxy_adapter/commands/protocol_management_command.py +232 -0
- mcp_proxy_adapter/commands/proxy_registration_command.py +409 -0
- mcp_proxy_adapter/commands/reload_command.py +48 -50
- mcp_proxy_adapter/commands/result.py +1 -0
- mcp_proxy_adapter/commands/role_test_command.py +141 -0
- mcp_proxy_adapter/commands/roles_management_command.py +697 -0
- mcp_proxy_adapter/commands/security_command.py +488 -0
- mcp_proxy_adapter/commands/ssl_setup_command.py +366 -0
- mcp_proxy_adapter/commands/token_management_command.py +529 -0
- mcp_proxy_adapter/commands/transport_management_command.py +144 -0
- mcp_proxy_adapter/commands/unload_command.py +158 -0
- mcp_proxy_adapter/config.py +394 -14
- mcp_proxy_adapter/core/app_factory.py +410 -0
- mcp_proxy_adapter/core/app_runner.py +272 -0
- mcp_proxy_adapter/core/auth_validator.py +606 -0
- mcp_proxy_adapter/core/certificate_utils.py +1045 -0
- mcp_proxy_adapter/core/client.py +574 -0
- mcp_proxy_adapter/core/client_manager.py +284 -0
- mcp_proxy_adapter/core/client_security.py +384 -0
- mcp_proxy_adapter/core/config_converter.py +405 -0
- mcp_proxy_adapter/core/config_validator.py +218 -0
- mcp_proxy_adapter/core/logging.py +19 -3
- mcp_proxy_adapter/core/mtls_asgi.py +156 -0
- mcp_proxy_adapter/core/mtls_asgi_app.py +187 -0
- mcp_proxy_adapter/core/protocol_manager.py +385 -0
- mcp_proxy_adapter/core/proxy_client.py +602 -0
- mcp_proxy_adapter/core/proxy_registration.py +522 -0
- mcp_proxy_adapter/core/role_utils.py +426 -0
- mcp_proxy_adapter/core/security_adapter.py +370 -0
- mcp_proxy_adapter/core/security_factory.py +239 -0
- mcp_proxy_adapter/core/security_integration.py +286 -0
- mcp_proxy_adapter/core/server_adapter.py +282 -0
- mcp_proxy_adapter/core/server_engine.py +270 -0
- mcp_proxy_adapter/core/settings.py +1 -0
- mcp_proxy_adapter/core/ssl_utils.py +234 -0
- mcp_proxy_adapter/core/transport_manager.py +292 -0
- mcp_proxy_adapter/core/unified_config_adapter.py +579 -0
- mcp_proxy_adapter/custom_openapi.py +22 -11
- mcp_proxy_adapter/examples/__init__.py +13 -4
- mcp_proxy_adapter/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/commands/__init__.py +5 -0
- mcp_proxy_adapter/examples/create_certificates_simple.py +550 -0
- mcp_proxy_adapter/examples/debug_request_state.py +112 -0
- mcp_proxy_adapter/examples/debug_role_chain.py +158 -0
- mcp_proxy_adapter/examples/demo_client.py +275 -0
- mcp_proxy_adapter/examples/examples/basic_framework/__init__.py +9 -0
- mcp_proxy_adapter/examples/examples/basic_framework/commands/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/hooks/__init__.py +4 -0
- mcp_proxy_adapter/examples/examples/basic_framework/main.py +44 -0
- mcp_proxy_adapter/examples/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/full_application/__init__.py +12 -0
- mcp_proxy_adapter/examples/full_application/commands/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +80 -0
- mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +90 -0
- mcp_proxy_adapter/examples/full_application/hooks/__init__.py +7 -0
- mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +75 -0
- mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +71 -0
- mcp_proxy_adapter/examples/full_application/main.py +173 -0
- mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +154 -0
- mcp_proxy_adapter/examples/generate_all_certificates.py +362 -0
- mcp_proxy_adapter/examples/generate_certificates.py +177 -0
- mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/generate_test_configs.py +331 -0
- mcp_proxy_adapter/examples/proxy_registration_example.py +334 -0
- mcp_proxy_adapter/examples/run_example.py +59 -0
- mcp_proxy_adapter/examples/run_full_test_suite.py +318 -0
- mcp_proxy_adapter/examples/run_proxy_server.py +146 -0
- mcp_proxy_adapter/examples/run_security_tests.py +544 -0
- mcp_proxy_adapter/examples/run_security_tests_fixed.py +247 -0
- mcp_proxy_adapter/examples/scripts/config_generator.py +740 -0
- mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +560 -0
- mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +369 -0
- mcp_proxy_adapter/examples/security_test_client.py +782 -0
- mcp_proxy_adapter/examples/setup_test_environment.py +328 -0
- mcp_proxy_adapter/examples/test_config.py +148 -0
- mcp_proxy_adapter/examples/test_config_generator.py +86 -0
- mcp_proxy_adapter/examples/test_examples.py +281 -0
- mcp_proxy_adapter/examples/universal_client.py +620 -0
- mcp_proxy_adapter/main.py +93 -0
- mcp_proxy_adapter/utils/config_generator.py +1008 -0
- mcp_proxy_adapter/version.py +5 -2
- mcp_proxy_adapter-6.0.1.dist-info/METADATA +679 -0
- mcp_proxy_adapter-6.0.1.dist-info/RECORD +140 -0
- mcp_proxy_adapter-6.0.1.dist-info/entry_points.txt +2 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/licenses/LICENSE +2 -2
- mcp_proxy_adapter/api/middleware/auth.py +0 -146
- mcp_proxy_adapter/api/middleware/rate_limit.py +0 -152
- mcp_proxy_adapter/commands/reload_settings_command.py +0 -125
- mcp_proxy_adapter/examples/README.md +0 -124
- mcp_proxy_adapter/examples/basic_server/README.md +0 -60
- mcp_proxy_adapter/examples/basic_server/__init__.py +0 -7
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +0 -39
- mcp_proxy_adapter/examples/basic_server/config.json +0 -35
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +0 -238
- mcp_proxy_adapter/examples/basic_server/server.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/README.md +0 -127
- mcp_proxy_adapter/examples/custom_commands/__init__.py +0 -27
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +0 -250
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +0 -6
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +0 -111
- mcp_proxy_adapter/examples/custom_commands/config.json +0 -35
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +0 -169
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +0 -215
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +0 -76
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +0 -96
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +0 -241
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +0 -135
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +0 -122
- mcp_proxy_adapter/examples/custom_commands/hooks.py +0 -230
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +0 -123
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +0 -103
- mcp_proxy_adapter/examples/custom_commands/server.py +0 -228
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +0 -176
- mcp_proxy_adapter/examples/deployment/README.md +0 -49
- mcp_proxy_adapter/examples/deployment/__init__.py +0 -7
- mcp_proxy_adapter/examples/deployment/config.development.json +0 -8
- mcp_proxy_adapter/examples/deployment/config.json +0 -29
- mcp_proxy_adapter/examples/deployment/config.production.json +0 -12
- mcp_proxy_adapter/examples/deployment/config.staging.json +0 -11
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +0 -31
- mcp_proxy_adapter/examples/deployment/run.sh +0 -43
- mcp_proxy_adapter/examples/deployment/run_docker.sh +0 -84
- mcp_proxy_adapter/schemas/base_schema.json +0 -114
- mcp_proxy_adapter/schemas/openapi_schema.json +0 -314
- mcp_proxy_adapter/tests/__init__.py +0 -0
- mcp_proxy_adapter/tests/api/__init__.py +0 -3
- mcp_proxy_adapter/tests/api/test_cmd_endpoint.py +0 -115
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +0 -617
- mcp_proxy_adapter/tests/api/test_handlers.py +0 -522
- mcp_proxy_adapter/tests/api/test_middleware.py +0 -340
- mcp_proxy_adapter/tests/api/test_schemas.py +0 -546
- mcp_proxy_adapter/tests/api/test_tool_integration.py +0 -531
- mcp_proxy_adapter/tests/commands/__init__.py +0 -3
- mcp_proxy_adapter/tests/commands/test_config_command.py +0 -211
- mcp_proxy_adapter/tests/commands/test_echo_command.py +0 -127
- mcp_proxy_adapter/tests/commands/test_help_command.py +0 -136
- mcp_proxy_adapter/tests/conftest.py +0 -131
- mcp_proxy_adapter/tests/functional/__init__.py +0 -3
- mcp_proxy_adapter/tests/functional/test_api.py +0 -253
- mcp_proxy_adapter/tests/integration/__init__.py +0 -3
- mcp_proxy_adapter/tests/integration/test_cmd_integration.py +0 -129
- mcp_proxy_adapter/tests/integration/test_integration.py +0 -255
- mcp_proxy_adapter/tests/performance/__init__.py +0 -3
- mcp_proxy_adapter/tests/performance/test_performance.py +0 -189
- mcp_proxy_adapter/tests/stubs/__init__.py +0 -10
- mcp_proxy_adapter/tests/stubs/echo_command.py +0 -104
- mcp_proxy_adapter/tests/test_api_endpoints.py +0 -271
- mcp_proxy_adapter/tests/test_api_handlers.py +0 -289
- mcp_proxy_adapter/tests/test_base_command.py +0 -123
- mcp_proxy_adapter/tests/test_batch_requests.py +0 -117
- mcp_proxy_adapter/tests/test_command_registry.py +0 -281
- mcp_proxy_adapter/tests/test_config.py +0 -127
- mcp_proxy_adapter/tests/test_utils.py +0 -65
- mcp_proxy_adapter/tests/unit/__init__.py +0 -3
- mcp_proxy_adapter/tests/unit/test_base_command.py +0 -436
- mcp_proxy_adapter/tests/unit/test_config.py +0 -217
- mcp_proxy_adapter-4.1.1.dist-info/METADATA +0 -200
- mcp_proxy_adapter-4.1.1.dist-info/RECORD +0 -110
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/WHEEL +0 -0
- {mcp_proxy_adapter-4.1.1.dist-info → mcp_proxy_adapter-6.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,838 @@
|
|
1
|
+
"""
|
2
|
+
Command catalog management system.
|
3
|
+
|
4
|
+
This module handles the command catalog, including:
|
5
|
+
- Loading commands from remote plugin servers
|
6
|
+
- Version comparison and updates
|
7
|
+
- Local command storage and management
|
8
|
+
- Automatic dependency installation
|
9
|
+
"""
|
10
|
+
|
11
|
+
import json
|
12
|
+
import os
|
13
|
+
import shutil
|
14
|
+
import tempfile
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import Dict, List, Optional, Any
|
17
|
+
from packaging import version as pkg_version
|
18
|
+
|
19
|
+
from mcp_proxy_adapter.core.logging import logger
|
20
|
+
from mcp_proxy_adapter.commands.dependency_manager import dependency_manager
|
21
|
+
from mcp_proxy_adapter.config import config
|
22
|
+
|
23
|
+
# Try to import requests, but don't fail if not available
|
24
|
+
try:
|
25
|
+
import requests
|
26
|
+
REQUESTS_AVAILABLE = True
|
27
|
+
except ImportError:
|
28
|
+
REQUESTS_AVAILABLE = False
|
29
|
+
logger.warning("requests library not available, HTTP/HTTPS functionality will be limited")
|
30
|
+
|
31
|
+
|
32
|
+
class CommandCatalog:
|
33
|
+
"""
|
34
|
+
Represents a command in the catalog with metadata.
|
35
|
+
"""
|
36
|
+
|
37
|
+
def __init__(self, name: str, version: str, source_url: str, file_path: Optional[str] = None):
|
38
|
+
self.name = name
|
39
|
+
self.version = version
|
40
|
+
self.source_url = source_url
|
41
|
+
self.file_path = file_path
|
42
|
+
self.metadata: Dict[str, Any] = {}
|
43
|
+
|
44
|
+
# Standard metadata fields
|
45
|
+
self.plugin: Optional[str] = None
|
46
|
+
self.descr: Optional[str] = None
|
47
|
+
self.category: Optional[str] = None
|
48
|
+
self.author: Optional[str] = None
|
49
|
+
self.email: Optional[str] = None
|
50
|
+
self.depends: Optional[List[str]] = None # New field for dependencies
|
51
|
+
|
52
|
+
def to_dict(self) -> Dict[str, Any]:
|
53
|
+
"""Convert to dictionary for serialization."""
|
54
|
+
return {
|
55
|
+
"name": self.name,
|
56
|
+
"version": self.version,
|
57
|
+
"source_url": self.source_url,
|
58
|
+
"file_path": self.file_path,
|
59
|
+
"plugin": self.plugin,
|
60
|
+
"descr": self.descr,
|
61
|
+
"category": self.category,
|
62
|
+
"author": self.author,
|
63
|
+
"email": self.email,
|
64
|
+
"depends": self.depends,
|
65
|
+
"metadata": self.metadata
|
66
|
+
}
|
67
|
+
|
68
|
+
@classmethod
|
69
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'CommandCatalog':
|
70
|
+
"""Create from dictionary."""
|
71
|
+
catalog = cls(
|
72
|
+
name=data["name"],
|
73
|
+
version=data["version"],
|
74
|
+
source_url=data["source_url"],
|
75
|
+
file_path=data.get("file_path")
|
76
|
+
)
|
77
|
+
|
78
|
+
# Load standard metadata fields
|
79
|
+
catalog.plugin = data.get("plugin")
|
80
|
+
catalog.descr = data.get("descr")
|
81
|
+
catalog.category = data.get("category")
|
82
|
+
catalog.author = data.get("author")
|
83
|
+
catalog.email = data.get("email")
|
84
|
+
|
85
|
+
# Handle depends field
|
86
|
+
depends = data.get("depends")
|
87
|
+
if depends and isinstance(depends, list):
|
88
|
+
catalog.depends = depends
|
89
|
+
elif depends and isinstance(depends, str):
|
90
|
+
catalog.depends = [depends]
|
91
|
+
else:
|
92
|
+
catalog.depends = depends
|
93
|
+
|
94
|
+
catalog.metadata = data.get("metadata", {})
|
95
|
+
|
96
|
+
return catalog
|
97
|
+
|
98
|
+
|
99
|
+
class CatalogManager:
|
100
|
+
"""
|
101
|
+
Manages the command catalog system.
|
102
|
+
|
103
|
+
The catalog is loaded fresh from servers each time - no caching.
|
104
|
+
It only contains the list of available plugins.
|
105
|
+
"""
|
106
|
+
|
107
|
+
def __init__(self, catalog_dir: str):
|
108
|
+
"""
|
109
|
+
Initialize catalog manager.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
catalog_dir: Directory to store downloaded command files
|
113
|
+
"""
|
114
|
+
self.catalog_dir = Path(catalog_dir)
|
115
|
+
self.commands_dir = self.catalog_dir / "commands"
|
116
|
+
|
117
|
+
# Ensure directories exist
|
118
|
+
self.catalog_dir.mkdir(parents=True, exist_ok=True)
|
119
|
+
self.commands_dir.mkdir(parents=True, exist_ok=True)
|
120
|
+
|
121
|
+
# No local catalog caching - always fetch fresh from servers
|
122
|
+
self.catalog: Dict[str, CommandCatalog] = {}
|
123
|
+
|
124
|
+
def _load_catalog(self) -> None:
|
125
|
+
"""DEPRECATED: Catalog is not cached locally."""
|
126
|
+
logger.warning("_load_catalog() is deprecated - catalog is always fetched fresh from servers")
|
127
|
+
|
128
|
+
def _save_catalog(self) -> None:
|
129
|
+
"""DEPRECATED: Catalog is not cached locally."""
|
130
|
+
logger.warning("_save_catalog() is deprecated - catalog is always fetched fresh from servers")
|
131
|
+
|
132
|
+
def _parse_catalog_data(self, server_catalog: Dict[str, Any], server_url: str = "http://example.com") -> Dict[str, CommandCatalog]:
|
133
|
+
"""
|
134
|
+
Parse catalog data from server response.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
server_catalog: Raw catalog data from server
|
138
|
+
server_url: URL of the server (for constructing source URLs)
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Dictionary of parsed CommandCatalog objects
|
142
|
+
"""
|
143
|
+
if not isinstance(server_catalog, dict):
|
144
|
+
logger.error(f"Invalid catalog format: expected dict, got {type(server_catalog)}")
|
145
|
+
return {}
|
146
|
+
|
147
|
+
result = {}
|
148
|
+
|
149
|
+
# Support both old format (commands array) and new format (id-value pairs)
|
150
|
+
if "commands" in server_catalog:
|
151
|
+
# Old format: {"commands": [{"name": "...", ...}]}
|
152
|
+
commands_data = server_catalog.get("commands", [])
|
153
|
+
|
154
|
+
if not isinstance(commands_data, list):
|
155
|
+
logger.error(f"Invalid commands format: expected list, got {type(commands_data)}")
|
156
|
+
return {}
|
157
|
+
|
158
|
+
for cmd_data in commands_data:
|
159
|
+
try:
|
160
|
+
if not isinstance(cmd_data, dict):
|
161
|
+
logger.warning(f"Skipping invalid command data: expected dict, got {type(cmd_data)}")
|
162
|
+
continue
|
163
|
+
|
164
|
+
name = cmd_data.get("name")
|
165
|
+
if not name or not isinstance(name, str):
|
166
|
+
logger.warning(f"Skipping command without valid name")
|
167
|
+
continue
|
168
|
+
|
169
|
+
version = cmd_data.get("version", "0.1")
|
170
|
+
if not isinstance(version, str):
|
171
|
+
logger.warning(f"Invalid version format for {name}: {version}")
|
172
|
+
version = "0.1"
|
173
|
+
|
174
|
+
source_url = cmd_data.get("source_url", "")
|
175
|
+
if not isinstance(source_url, str):
|
176
|
+
logger.warning(f"Invalid source_url for {name}: {source_url}")
|
177
|
+
source_url = ""
|
178
|
+
|
179
|
+
catalog = CommandCatalog(
|
180
|
+
name=name,
|
181
|
+
version=version,
|
182
|
+
source_url=source_url
|
183
|
+
)
|
184
|
+
|
185
|
+
# Load standard metadata fields
|
186
|
+
catalog.plugin = cmd_data.get("plugin")
|
187
|
+
catalog.descr = cmd_data.get("descr")
|
188
|
+
catalog.category = cmd_data.get("category")
|
189
|
+
catalog.author = cmd_data.get("author")
|
190
|
+
catalog.email = cmd_data.get("email")
|
191
|
+
catalog.metadata = cmd_data
|
192
|
+
|
193
|
+
result[name] = catalog
|
194
|
+
|
195
|
+
except Exception as e:
|
196
|
+
logger.error(f"Error processing command data: {e}")
|
197
|
+
continue
|
198
|
+
else:
|
199
|
+
# New format: {"id": {"plugin": "...", "descr": "...", ...}}
|
200
|
+
for command_id, cmd_data in server_catalog.items():
|
201
|
+
try:
|
202
|
+
if not isinstance(cmd_data, dict):
|
203
|
+
logger.warning(f"Skipping invalid command data for {command_id}: expected dict, got {type(cmd_data)}")
|
204
|
+
continue
|
205
|
+
|
206
|
+
# Use command_id as name if name is not provided
|
207
|
+
name = cmd_data.get("name", command_id)
|
208
|
+
if not isinstance(name, str):
|
209
|
+
logger.warning(f"Skipping command {command_id} without valid name")
|
210
|
+
continue
|
211
|
+
|
212
|
+
version = cmd_data.get("version", "0.1")
|
213
|
+
if not isinstance(version, str):
|
214
|
+
logger.warning(f"Invalid version format for {name}: {version}")
|
215
|
+
version = "0.1"
|
216
|
+
|
217
|
+
# For new format, construct source_url from server_url and plugin filename
|
218
|
+
plugin_file = cmd_data.get("plugin")
|
219
|
+
if plugin_file and isinstance(plugin_file, str):
|
220
|
+
# Construct source URL by appending plugin filename to server URL
|
221
|
+
if server_url.endswith('/'):
|
222
|
+
source_url = f"{server_url}{plugin_file}"
|
223
|
+
else:
|
224
|
+
source_url = f"{server_url}/{plugin_file}"
|
225
|
+
else:
|
226
|
+
source_url = server_url
|
227
|
+
|
228
|
+
catalog = CommandCatalog(
|
229
|
+
name=name,
|
230
|
+
version=version,
|
231
|
+
source_url=source_url
|
232
|
+
)
|
233
|
+
|
234
|
+
# Load standard metadata fields
|
235
|
+
catalog.plugin = cmd_data.get("plugin")
|
236
|
+
catalog.descr = cmd_data.get("descr")
|
237
|
+
catalog.category = cmd_data.get("category")
|
238
|
+
catalog.author = cmd_data.get("author")
|
239
|
+
catalog.email = cmd_data.get("email")
|
240
|
+
|
241
|
+
# Load dependencies field
|
242
|
+
depends = cmd_data.get("depends")
|
243
|
+
if depends and isinstance(depends, list):
|
244
|
+
catalog.depends = depends
|
245
|
+
elif depends and isinstance(depends, str):
|
246
|
+
# Handle case where depends is a single string
|
247
|
+
catalog.depends = [depends]
|
248
|
+
|
249
|
+
# Store full metadata including new fields like "depends"
|
250
|
+
catalog.metadata = cmd_data
|
251
|
+
|
252
|
+
result[name] = catalog
|
253
|
+
|
254
|
+
except Exception as e:
|
255
|
+
logger.error(f"Error processing command {command_id}: {e}")
|
256
|
+
continue
|
257
|
+
|
258
|
+
return result
|
259
|
+
|
260
|
+
def get_catalog_from_server(self, server_url: str) -> Dict[str, CommandCatalog]:
|
261
|
+
"""
|
262
|
+
Fetch command catalog from remote server.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
server_url: URL of the plugin server
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Dictionary of commands available on the server
|
269
|
+
"""
|
270
|
+
if not REQUESTS_AVAILABLE:
|
271
|
+
logger.error("requests library not available, cannot fetch catalog from remote server")
|
272
|
+
return {}
|
273
|
+
|
274
|
+
try:
|
275
|
+
# Validate URL format
|
276
|
+
if not server_url.startswith(('http://', 'https://')):
|
277
|
+
logger.error(f"Invalid server URL format: {server_url}")
|
278
|
+
return {}
|
279
|
+
|
280
|
+
# Fetch catalog from server (use URL as-is)
|
281
|
+
logger.debug(f"Fetching catalog from: {server_url}")
|
282
|
+
|
283
|
+
response = requests.get(server_url, timeout=30)
|
284
|
+
response.raise_for_status()
|
285
|
+
|
286
|
+
# Validate response content
|
287
|
+
if not response.content:
|
288
|
+
logger.error(f"Empty response from {server_url}")
|
289
|
+
return {}
|
290
|
+
|
291
|
+
try:
|
292
|
+
server_catalog = response.json()
|
293
|
+
except json.JSONDecodeError as e:
|
294
|
+
logger.error(f"Invalid JSON response from {server_url}: {e}")
|
295
|
+
return {}
|
296
|
+
|
297
|
+
if not isinstance(server_catalog, dict):
|
298
|
+
logger.error(f"Invalid catalog format from {server_url}: expected dict, got {type(server_catalog)}")
|
299
|
+
return {}
|
300
|
+
|
301
|
+
result = self._parse_catalog_data(server_catalog, server_url)
|
302
|
+
|
303
|
+
logger.info(f"Successfully fetched catalog from {server_url}: {len(result)} commands")
|
304
|
+
return result
|
305
|
+
|
306
|
+
except requests.exceptions.Timeout:
|
307
|
+
logger.error(f"Timeout while fetching catalog from {server_url}")
|
308
|
+
return {}
|
309
|
+
except requests.exceptions.ConnectionError as e:
|
310
|
+
logger.error(f"Connection error while fetching catalog from {server_url}: {e}")
|
311
|
+
return {}
|
312
|
+
except requests.exceptions.HTTPError as e:
|
313
|
+
logger.error(f"HTTP error while fetching catalog from {server_url}: {e}")
|
314
|
+
return {}
|
315
|
+
except requests.exceptions.RequestException as e:
|
316
|
+
logger.error(f"Request error while fetching catalog from {server_url}: {e}")
|
317
|
+
return {}
|
318
|
+
except Exception as e:
|
319
|
+
logger.error(f"Unexpected error while fetching catalog from {server_url}: {e}")
|
320
|
+
return {}
|
321
|
+
|
322
|
+
def _check_dependencies(self, command_name: str, server_cmd: CommandCatalog) -> bool:
|
323
|
+
"""
|
324
|
+
Check if command dependencies are satisfied.
|
325
|
+
|
326
|
+
Args:
|
327
|
+
command_name: Name of the command
|
328
|
+
server_cmd: Command catalog entry from server
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
True if dependencies are satisfied, False otherwise
|
332
|
+
"""
|
333
|
+
if not server_cmd.depends:
|
334
|
+
return True
|
335
|
+
|
336
|
+
all_satisfied, missing_deps, installed_deps = dependency_manager.check_dependencies(server_cmd.depends)
|
337
|
+
|
338
|
+
if not all_satisfied:
|
339
|
+
logger.warning(f"Command {command_name} has missing dependencies: {missing_deps}")
|
340
|
+
logger.info(f"Installed dependencies: {installed_deps}")
|
341
|
+
return False
|
342
|
+
|
343
|
+
logger.debug(f"All dependencies satisfied for command {command_name}: {server_cmd.depends}")
|
344
|
+
return True
|
345
|
+
|
346
|
+
def _install_dependencies(self, command_name: str, server_cmd: CommandCatalog, auto_install: bool = None) -> bool:
|
347
|
+
"""
|
348
|
+
Install command dependencies automatically.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
command_name: Name of the command
|
352
|
+
server_cmd: Command catalog entry from server
|
353
|
+
auto_install: Whether to automatically install missing dependencies (None = use config)
|
354
|
+
|
355
|
+
Returns:
|
356
|
+
True if all dependencies are satisfied, False otherwise
|
357
|
+
"""
|
358
|
+
if not server_cmd.depends:
|
359
|
+
return True
|
360
|
+
|
361
|
+
# Use config setting if auto_install is None
|
362
|
+
if auto_install is None:
|
363
|
+
auto_install = config.get("commands.auto_install_dependencies", True)
|
364
|
+
|
365
|
+
# Check current dependencies
|
366
|
+
all_satisfied, missing_deps, installed_deps = dependency_manager.check_dependencies(server_cmd.depends)
|
367
|
+
|
368
|
+
if all_satisfied:
|
369
|
+
logger.info(f"All dependencies already satisfied for {command_name}: {server_cmd.depends}")
|
370
|
+
return True
|
371
|
+
|
372
|
+
if not auto_install:
|
373
|
+
logger.warning(f"Command {command_name} has missing dependencies: {missing_deps}")
|
374
|
+
logger.info(f"Auto-install is disabled. Please install manually: pip install {' '.join(missing_deps)}")
|
375
|
+
return False
|
376
|
+
|
377
|
+
# Try to install missing dependencies
|
378
|
+
logger.info(f"Installing missing dependencies for {command_name}: {missing_deps}")
|
379
|
+
|
380
|
+
success, installed_deps, failed_deps = dependency_manager.install_dependencies(missing_deps)
|
381
|
+
|
382
|
+
if success:
|
383
|
+
logger.info(f"Successfully installed all dependencies for {command_name}: {installed_deps}")
|
384
|
+
|
385
|
+
# Verify installation
|
386
|
+
all_verified, failed_verifications = dependency_manager.verify_installation(server_cmd.depends)
|
387
|
+
if all_verified:
|
388
|
+
logger.info(f"All dependencies verified for {command_name}")
|
389
|
+
return True
|
390
|
+
else:
|
391
|
+
logger.error(f"Failed to verify dependencies for {command_name}: {failed_verifications}")
|
392
|
+
return False
|
393
|
+
else:
|
394
|
+
logger.error(f"Failed to install dependencies for {command_name}: {failed_deps}")
|
395
|
+
logger.error(f"Please install manually: pip install {' '.join(failed_deps)}")
|
396
|
+
return False
|
397
|
+
|
398
|
+
def _should_download_command(self, command_name: str, server_cmd: CommandCatalog) -> bool:
|
399
|
+
"""
|
400
|
+
Check if command should be downloaded based on version comparison.
|
401
|
+
|
402
|
+
Args:
|
403
|
+
command_name: Name of the command
|
404
|
+
server_cmd: Command catalog entry from server
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
True if command should be downloaded, False otherwise
|
408
|
+
"""
|
409
|
+
local_file = self.commands_dir / f"{command_name}_command.py"
|
410
|
+
|
411
|
+
# If local file doesn't exist, download
|
412
|
+
if not local_file.exists():
|
413
|
+
logger.info(f"New command found: {command_name} v{server_cmd.version}")
|
414
|
+
return True
|
415
|
+
|
416
|
+
# Try to extract version from local file
|
417
|
+
try:
|
418
|
+
local_metadata = self.extract_metadata_from_file(str(local_file))
|
419
|
+
local_version = local_metadata.get("version", "0.0")
|
420
|
+
|
421
|
+
# Compare versions
|
422
|
+
server_ver = pkg_version.parse(server_cmd.version)
|
423
|
+
local_ver = pkg_version.parse(local_version)
|
424
|
+
|
425
|
+
if server_ver > local_ver:
|
426
|
+
logger.info(f"Newer version available for {command_name}: {local_ver} -> {server_ver}")
|
427
|
+
return True
|
428
|
+
else:
|
429
|
+
logger.debug(f"Local version {local_ver} is same or newer than server version {server_ver}")
|
430
|
+
return False
|
431
|
+
|
432
|
+
except Exception as e:
|
433
|
+
logger.warning(f"Failed to compare versions for {command_name}: {e}")
|
434
|
+
# If version comparison fails, download anyway
|
435
|
+
return True
|
436
|
+
|
437
|
+
def update_command(self, command_name: str, server_catalog: Dict[str, CommandCatalog]) -> bool:
|
438
|
+
"""
|
439
|
+
DEPRECATED: Always download fresh from server.
|
440
|
+
|
441
|
+
Args:
|
442
|
+
command_name: Name of the command to update
|
443
|
+
server_catalog: Catalog from server
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
True if command was downloaded, False otherwise
|
447
|
+
"""
|
448
|
+
logger.warning("update_command() is deprecated - always downloading fresh from server (use _download_command directly)")
|
449
|
+
|
450
|
+
if command_name not in server_catalog:
|
451
|
+
return False
|
452
|
+
|
453
|
+
server_cmd = server_catalog[command_name]
|
454
|
+
return self._download_command(command_name, server_cmd)
|
455
|
+
|
456
|
+
def _download_command(self, command_name: str, server_cmd: CommandCatalog) -> bool:
|
457
|
+
"""
|
458
|
+
Download command file from server.
|
459
|
+
|
460
|
+
Args:
|
461
|
+
command_name: Name of the command
|
462
|
+
server_cmd: Command catalog entry from server
|
463
|
+
|
464
|
+
Returns:
|
465
|
+
True if download successful, False otherwise
|
466
|
+
"""
|
467
|
+
if not REQUESTS_AVAILABLE:
|
468
|
+
logger.error("requests library not available, cannot download command from remote server")
|
469
|
+
return False
|
470
|
+
|
471
|
+
# Step 1: Check and install dependencies
|
472
|
+
if not self._install_dependencies(command_name, server_cmd):
|
473
|
+
logger.error(f"Cannot download {command_name}: failed to install dependencies")
|
474
|
+
return False
|
475
|
+
|
476
|
+
try:
|
477
|
+
# Validate source URL
|
478
|
+
if not server_cmd.source_url.startswith(('http://', 'https://')):
|
479
|
+
logger.error(f"Invalid source URL for {command_name}: {server_cmd.source_url}")
|
480
|
+
return False
|
481
|
+
|
482
|
+
# Download command file (use source_url as-is)
|
483
|
+
logger.debug(f"Downloading {command_name} from: {server_cmd.source_url}")
|
484
|
+
|
485
|
+
response = requests.get(server_cmd.source_url, timeout=30)
|
486
|
+
response.raise_for_status()
|
487
|
+
|
488
|
+
# Validate response content
|
489
|
+
if not response.content:
|
490
|
+
logger.error(f"Empty response when downloading {command_name} from {server_cmd.source_url}")
|
491
|
+
return False
|
492
|
+
|
493
|
+
# Validate Python file content
|
494
|
+
content = response.text
|
495
|
+
if not content.strip():
|
496
|
+
logger.error(f"Empty file content for {command_name} from {server_cmd.source_url}")
|
497
|
+
return False
|
498
|
+
|
499
|
+
# Basic Python file validation - only warn for clearly invalid files
|
500
|
+
content_stripped = content.strip()
|
501
|
+
if (content_stripped and
|
502
|
+
not content_stripped.startswith(('"""', "'''", '#', 'from ', 'import ', 'class ', 'def ')) and
|
503
|
+
not any(keyword in content_stripped for keyword in ['class', 'def', 'import', 'from', '"""', "'''", '#'])):
|
504
|
+
logger.warning(f"File {command_name}_command.py doesn't look like a valid Python file")
|
505
|
+
|
506
|
+
# Create temporary file first for validation
|
507
|
+
temp_file = None
|
508
|
+
try:
|
509
|
+
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False)
|
510
|
+
temp_file.write(content)
|
511
|
+
temp_file.close()
|
512
|
+
|
513
|
+
# Try to import the module to validate it
|
514
|
+
try:
|
515
|
+
import importlib.util
|
516
|
+
spec = importlib.util.spec_from_file_location(f"{command_name}_command", temp_file.name)
|
517
|
+
if spec and spec.loader:
|
518
|
+
module = importlib.util.module_from_spec(spec)
|
519
|
+
spec.loader.exec_module(module)
|
520
|
+
|
521
|
+
# Check if module contains a Command class
|
522
|
+
if not hasattr(module, 'Command') and not any(
|
523
|
+
hasattr(module, attr) and attr.endswith('Command')
|
524
|
+
for attr in dir(module)
|
525
|
+
):
|
526
|
+
logger.warning(f"Module {command_name}_command.py doesn't contain a Command class")
|
527
|
+
|
528
|
+
except Exception as e:
|
529
|
+
logger.error(f"Failed to validate {command_name}_command.py: {e}")
|
530
|
+
return False
|
531
|
+
|
532
|
+
# If validation passed, move to final location
|
533
|
+
file_path = self.commands_dir / f"{command_name}_command.py"
|
534
|
+
|
535
|
+
# Remove existing file if it exists
|
536
|
+
if file_path.exists():
|
537
|
+
file_path.unlink()
|
538
|
+
|
539
|
+
# Move temporary file to final location
|
540
|
+
shutil.move(temp_file.name, file_path)
|
541
|
+
|
542
|
+
# Update catalog entry
|
543
|
+
server_cmd.file_path = str(file_path)
|
544
|
+
self.catalog[command_name] = server_cmd
|
545
|
+
|
546
|
+
# Extract metadata from downloaded file
|
547
|
+
try:
|
548
|
+
metadata = self.extract_metadata_from_file(str(file_path))
|
549
|
+
if metadata:
|
550
|
+
# Update standard fields
|
551
|
+
if 'plugin' in metadata:
|
552
|
+
server_cmd.plugin = metadata['plugin']
|
553
|
+
if 'descr' in metadata:
|
554
|
+
server_cmd.descr = metadata['descr']
|
555
|
+
if 'category' in metadata:
|
556
|
+
server_cmd.category = metadata['category']
|
557
|
+
if 'author' in metadata:
|
558
|
+
server_cmd.author = metadata['author']
|
559
|
+
if 'email' in metadata:
|
560
|
+
server_cmd.email = metadata['email']
|
561
|
+
if 'version' in metadata:
|
562
|
+
server_cmd.version = metadata['version']
|
563
|
+
|
564
|
+
# Update full metadata
|
565
|
+
server_cmd.metadata.update(metadata)
|
566
|
+
|
567
|
+
logger.debug(f"Extracted metadata for {command_name}: {metadata}")
|
568
|
+
except Exception as e:
|
569
|
+
logger.warning(f"Failed to extract metadata from {command_name}: {e}")
|
570
|
+
|
571
|
+
logger.info(f"Successfully downloaded and validated {command_name} v{server_cmd.version}")
|
572
|
+
return True
|
573
|
+
|
574
|
+
finally:
|
575
|
+
# Clean up temporary file if it still exists
|
576
|
+
if temp_file and os.path.exists(temp_file.name):
|
577
|
+
try:
|
578
|
+
os.unlink(temp_file.name)
|
579
|
+
except Exception as e:
|
580
|
+
logger.warning(f"Failed to clean up temporary file {temp_file.name}: {e}")
|
581
|
+
|
582
|
+
except requests.exceptions.Timeout:
|
583
|
+
logger.error(f"Timeout while downloading {command_name} from {server_cmd.source_url}")
|
584
|
+
return False
|
585
|
+
except requests.exceptions.ConnectionError as e:
|
586
|
+
logger.error(f"Connection error while downloading {command_name} from {server_cmd.source_url}: {e}")
|
587
|
+
return False
|
588
|
+
except requests.exceptions.HTTPError as e:
|
589
|
+
logger.error(f"HTTP error while downloading {command_name} from {server_cmd.source_url}: {e}")
|
590
|
+
return False
|
591
|
+
except requests.exceptions.RequestException as e:
|
592
|
+
logger.error(f"Request error while downloading {command_name} from {server_cmd.source_url}: {e}")
|
593
|
+
return False
|
594
|
+
except OSError as e:
|
595
|
+
logger.error(f"File system error while downloading {command_name}: {e}")
|
596
|
+
return False
|
597
|
+
except Exception as e:
|
598
|
+
logger.error(f"Unexpected error while downloading {command_name}: {e}")
|
599
|
+
return False
|
600
|
+
|
601
|
+
def sync_with_servers(self, server_urls: List[str]) -> Dict[str, Any]:
|
602
|
+
"""
|
603
|
+
Sync with remote servers - load fresh catalog each time.
|
604
|
+
|
605
|
+
Args:
|
606
|
+
server_urls: List of server URLs to sync with
|
607
|
+
|
608
|
+
Returns:
|
609
|
+
Dictionary with sync results
|
610
|
+
"""
|
611
|
+
logger.info(f"Loading fresh catalog from {len(server_urls)} servers")
|
612
|
+
|
613
|
+
# Clear local catalog - always start fresh
|
614
|
+
self.catalog = {}
|
615
|
+
|
616
|
+
results = {
|
617
|
+
"servers_processed": 0,
|
618
|
+
"commands_updated": 0,
|
619
|
+
"commands_added": 0,
|
620
|
+
"errors": []
|
621
|
+
}
|
622
|
+
|
623
|
+
for server_url in server_urls:
|
624
|
+
try:
|
625
|
+
# Get fresh catalog from server
|
626
|
+
server_catalog = self.get_catalog_from_server(server_url)
|
627
|
+
if not server_catalog:
|
628
|
+
continue
|
629
|
+
|
630
|
+
results["servers_processed"] += 1
|
631
|
+
|
632
|
+
# Process each command from server catalog
|
633
|
+
for command_name, server_cmd in server_catalog.items():
|
634
|
+
# Check if we need to download/update
|
635
|
+
if self._should_download_command(command_name, server_cmd):
|
636
|
+
if self._download_command(command_name, server_cmd):
|
637
|
+
results["commands_added"] += 1
|
638
|
+
# Add to local catalog for this session
|
639
|
+
self.catalog[command_name] = server_cmd
|
640
|
+
else:
|
641
|
+
# Command already exists with same or newer version
|
642
|
+
logger.debug(f"Command {command_name} already exists with same or newer version")
|
643
|
+
|
644
|
+
except Exception as e:
|
645
|
+
error_msg = f"Failed to sync with {server_url}: {e}"
|
646
|
+
results["errors"].append(error_msg)
|
647
|
+
logger.error(error_msg)
|
648
|
+
|
649
|
+
logger.info(f"Fresh catalog loaded: {results}")
|
650
|
+
return results
|
651
|
+
|
652
|
+
def get_local_commands(self) -> List[str]:
|
653
|
+
"""
|
654
|
+
Get list of locally available command files.
|
655
|
+
|
656
|
+
Returns:
|
657
|
+
List of command file paths
|
658
|
+
"""
|
659
|
+
commands = []
|
660
|
+
for file_path in self.commands_dir.glob("*_command.py"):
|
661
|
+
commands.append(str(file_path))
|
662
|
+
|
663
|
+
logger.debug(f"Found {len(commands)} local command files: {[Path(c).name for c in commands]}")
|
664
|
+
return commands
|
665
|
+
|
666
|
+
def get_command_info(self, command_name: str) -> Optional[CommandCatalog]:
|
667
|
+
"""
|
668
|
+
Get information about a command in the catalog.
|
669
|
+
|
670
|
+
Args:
|
671
|
+
command_name: Name of the command
|
672
|
+
|
673
|
+
Returns:
|
674
|
+
Command catalog entry or None if not found
|
675
|
+
"""
|
676
|
+
return self.catalog.get(command_name)
|
677
|
+
|
678
|
+
def remove_command(self, command_name: str) -> bool:
|
679
|
+
"""
|
680
|
+
Remove command from catalog and delete file.
|
681
|
+
|
682
|
+
Args:
|
683
|
+
command_name: Name of the command to remove
|
684
|
+
|
685
|
+
Returns:
|
686
|
+
True if removed successfully, False otherwise
|
687
|
+
"""
|
688
|
+
if command_name not in self.catalog:
|
689
|
+
return False
|
690
|
+
|
691
|
+
try:
|
692
|
+
# Remove file
|
693
|
+
cmd = self.catalog[command_name]
|
694
|
+
if cmd.file_path and os.path.exists(cmd.file_path):
|
695
|
+
os.remove(cmd.file_path)
|
696
|
+
|
697
|
+
# Remove from catalog
|
698
|
+
del self.catalog[command_name]
|
699
|
+
self._save_catalog()
|
700
|
+
|
701
|
+
logger.info(f"Removed command: {command_name}")
|
702
|
+
return True
|
703
|
+
|
704
|
+
except Exception as e:
|
705
|
+
logger.error(f"Failed to remove command {command_name}: {e}")
|
706
|
+
return False
|
707
|
+
|
708
|
+
def extract_metadata_from_file(self, file_path: str) -> Dict[str, Any]:
|
709
|
+
"""
|
710
|
+
Extract metadata from command file.
|
711
|
+
|
712
|
+
Args:
|
713
|
+
file_path: Path to command file
|
714
|
+
|
715
|
+
Returns:
|
716
|
+
Dictionary with extracted metadata
|
717
|
+
"""
|
718
|
+
metadata = {}
|
719
|
+
|
720
|
+
try:
|
721
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
722
|
+
content = f.read()
|
723
|
+
|
724
|
+
# Look for metadata in docstring or comments
|
725
|
+
lines = content.split('\n')
|
726
|
+
|
727
|
+
for line in lines:
|
728
|
+
line = line.strip()
|
729
|
+
|
730
|
+
# Look for metadata patterns
|
731
|
+
if line.startswith('#') or line.startswith('"""') or line.startswith("'''"):
|
732
|
+
# Try to parse JSON-like metadata
|
733
|
+
if '{' in line and '}' in line:
|
734
|
+
try:
|
735
|
+
# Extract JSON part
|
736
|
+
start = line.find('{')
|
737
|
+
end = line.rfind('}') + 1
|
738
|
+
json_str = line[start:end]
|
739
|
+
|
740
|
+
# Parse JSON
|
741
|
+
import json
|
742
|
+
parsed = json.loads(json_str)
|
743
|
+
|
744
|
+
# Update metadata
|
745
|
+
metadata.update(parsed)
|
746
|
+
|
747
|
+
except json.JSONDecodeError:
|
748
|
+
# Not valid JSON, continue
|
749
|
+
continue
|
750
|
+
|
751
|
+
# Look for specific metadata patterns
|
752
|
+
for pattern in ['plugin:', 'descr:', 'category:', 'author:', 'version:', 'email:']:
|
753
|
+
if pattern in line:
|
754
|
+
key = pattern.rstrip(':')
|
755
|
+
value = line.split(pattern, 1)[1].strip().strip('"\'')
|
756
|
+
metadata[key] = value
|
757
|
+
|
758
|
+
# Also check for JSON in docstring blocks
|
759
|
+
content_str = content
|
760
|
+
if '"""' in content_str or "'''" in content_str:
|
761
|
+
# Find docstring blocks
|
762
|
+
import re
|
763
|
+
docstring_pattern = r'"""(.*?)"""|\'\'\'(.*?)\'\'\''
|
764
|
+
matches = re.findall(docstring_pattern, content_str, re.DOTALL)
|
765
|
+
|
766
|
+
for match in matches:
|
767
|
+
docstring = match[0] if match[0] else match[1]
|
768
|
+
# Look for JSON in docstring
|
769
|
+
if '{' in docstring and '}' in docstring:
|
770
|
+
try:
|
771
|
+
# Find JSON object
|
772
|
+
start = docstring.find('{')
|
773
|
+
end = docstring.rfind('}') + 1
|
774
|
+
json_str = docstring[start:end]
|
775
|
+
|
776
|
+
import json
|
777
|
+
parsed = json.loads(json_str)
|
778
|
+
metadata.update(parsed)
|
779
|
+
|
780
|
+
except json.JSONDecodeError:
|
781
|
+
continue
|
782
|
+
|
783
|
+
logger.debug(f"Extracted metadata from {file_path}: {metadata}")
|
784
|
+
return metadata
|
785
|
+
|
786
|
+
except Exception as e:
|
787
|
+
logger.error(f"Failed to extract metadata from {file_path}: {e}")
|
788
|
+
return {}
|
789
|
+
|
790
|
+
def update_local_command_metadata(self, command_name: str) -> bool:
|
791
|
+
"""
|
792
|
+
Update metadata for a local command by reading its file.
|
793
|
+
|
794
|
+
Args:
|
795
|
+
command_name: Name of the command
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
True if updated successfully, False otherwise
|
799
|
+
"""
|
800
|
+
if command_name not in self.catalog:
|
801
|
+
return False
|
802
|
+
|
803
|
+
cmd = self.catalog[command_name]
|
804
|
+
if not cmd.file_path or not os.path.exists(cmd.file_path):
|
805
|
+
return False
|
806
|
+
|
807
|
+
try:
|
808
|
+
metadata = self.extract_metadata_from_file(cmd.file_path)
|
809
|
+
|
810
|
+
if metadata:
|
811
|
+
# Update standard fields
|
812
|
+
if 'plugin' in metadata:
|
813
|
+
cmd.plugin = metadata['plugin']
|
814
|
+
if 'descr' in metadata:
|
815
|
+
cmd.descr = metadata['descr']
|
816
|
+
if 'category' in metadata:
|
817
|
+
cmd.category = metadata['category']
|
818
|
+
if 'author' in metadata:
|
819
|
+
cmd.author = metadata['author']
|
820
|
+
if 'email' in metadata:
|
821
|
+
cmd.email = metadata['email']
|
822
|
+
if 'version' in metadata:
|
823
|
+
cmd.version = metadata['version']
|
824
|
+
|
825
|
+
# Update full metadata
|
826
|
+
cmd.metadata.update(metadata)
|
827
|
+
|
828
|
+
# Save catalog
|
829
|
+
self._save_catalog()
|
830
|
+
|
831
|
+
logger.info(f"Updated metadata for {command_name}")
|
832
|
+
return True
|
833
|
+
|
834
|
+
return False
|
835
|
+
|
836
|
+
except Exception as e:
|
837
|
+
logger.error(f"Failed to update metadata for {command_name}: {e}")
|
838
|
+
return False
|