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