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.
Files changed (129) hide show
  1. mcp_proxy_adapter/__init__.py +9 -5
  2. mcp_proxy_adapter/__main__.py +1 -1
  3. mcp_proxy_adapter/api/app.py +227 -176
  4. mcp_proxy_adapter/api/handlers.py +68 -60
  5. mcp_proxy_adapter/api/middleware/__init__.py +7 -5
  6. mcp_proxy_adapter/api/middleware/base.py +19 -16
  7. mcp_proxy_adapter/api/middleware/command_permission_middleware.py +44 -34
  8. mcp_proxy_adapter/api/middleware/error_handling.py +57 -67
  9. mcp_proxy_adapter/api/middleware/factory.py +50 -52
  10. mcp_proxy_adapter/api/middleware/logging.py +46 -30
  11. mcp_proxy_adapter/api/middleware/performance.py +19 -16
  12. mcp_proxy_adapter/api/middleware/protocol_middleware.py +80 -50
  13. mcp_proxy_adapter/api/middleware/transport_middleware.py +26 -24
  14. mcp_proxy_adapter/api/middleware/unified_security.py +70 -51
  15. mcp_proxy_adapter/api/middleware/user_info_middleware.py +43 -34
  16. mcp_proxy_adapter/api/schemas.py +69 -43
  17. mcp_proxy_adapter/api/tool_integration.py +83 -63
  18. mcp_proxy_adapter/api/tools.py +60 -50
  19. mcp_proxy_adapter/commands/__init__.py +15 -6
  20. mcp_proxy_adapter/commands/auth_validation_command.py +107 -110
  21. mcp_proxy_adapter/commands/base.py +108 -112
  22. mcp_proxy_adapter/commands/builtin_commands.py +28 -18
  23. mcp_proxy_adapter/commands/catalog_manager.py +394 -265
  24. mcp_proxy_adapter/commands/cert_monitor_command.py +222 -204
  25. mcp_proxy_adapter/commands/certificate_management_command.py +210 -213
  26. mcp_proxy_adapter/commands/command_registry.py +275 -226
  27. mcp_proxy_adapter/commands/config_command.py +48 -33
  28. mcp_proxy_adapter/commands/dependency_container.py +22 -23
  29. mcp_proxy_adapter/commands/dependency_manager.py +65 -56
  30. mcp_proxy_adapter/commands/echo_command.py +15 -15
  31. mcp_proxy_adapter/commands/health_command.py +31 -29
  32. mcp_proxy_adapter/commands/help_command.py +97 -61
  33. mcp_proxy_adapter/commands/hooks.py +65 -49
  34. mcp_proxy_adapter/commands/key_management_command.py +148 -147
  35. mcp_proxy_adapter/commands/load_command.py +58 -40
  36. mcp_proxy_adapter/commands/plugins_command.py +80 -54
  37. mcp_proxy_adapter/commands/protocol_management_command.py +60 -48
  38. mcp_proxy_adapter/commands/proxy_registration_command.py +107 -115
  39. mcp_proxy_adapter/commands/reload_command.py +43 -37
  40. mcp_proxy_adapter/commands/result.py +26 -33
  41. mcp_proxy_adapter/commands/role_test_command.py +26 -26
  42. mcp_proxy_adapter/commands/roles_management_command.py +176 -173
  43. mcp_proxy_adapter/commands/security_command.py +134 -122
  44. mcp_proxy_adapter/commands/settings_command.py +47 -56
  45. mcp_proxy_adapter/commands/ssl_setup_command.py +109 -129
  46. mcp_proxy_adapter/commands/token_management_command.py +129 -158
  47. mcp_proxy_adapter/commands/transport_management_command.py +41 -36
  48. mcp_proxy_adapter/commands/unload_command.py +42 -37
  49. mcp_proxy_adapter/config.py +36 -35
  50. mcp_proxy_adapter/core/__init__.py +19 -21
  51. mcp_proxy_adapter/core/app_factory.py +30 -9
  52. mcp_proxy_adapter/core/app_runner.py +81 -64
  53. mcp_proxy_adapter/core/auth_validator.py +176 -182
  54. mcp_proxy_adapter/core/certificate_utils.py +469 -426
  55. mcp_proxy_adapter/core/client.py +155 -126
  56. mcp_proxy_adapter/core/client_manager.py +60 -54
  57. mcp_proxy_adapter/core/client_security.py +108 -88
  58. mcp_proxy_adapter/core/config_converter.py +176 -143
  59. mcp_proxy_adapter/core/config_validator.py +12 -4
  60. mcp_proxy_adapter/core/crl_utils.py +21 -7
  61. mcp_proxy_adapter/core/errors.py +64 -20
  62. mcp_proxy_adapter/core/logging.py +34 -29
  63. mcp_proxy_adapter/core/mtls_asgi.py +29 -25
  64. mcp_proxy_adapter/core/mtls_asgi_app.py +66 -54
  65. mcp_proxy_adapter/core/protocol_manager.py +154 -104
  66. mcp_proxy_adapter/core/proxy_client.py +202 -144
  67. mcp_proxy_adapter/core/proxy_registration.py +7 -3
  68. mcp_proxy_adapter/core/role_utils.py +139 -125
  69. mcp_proxy_adapter/core/security_adapter.py +88 -77
  70. mcp_proxy_adapter/core/security_factory.py +50 -44
  71. mcp_proxy_adapter/core/security_integration.py +72 -24
  72. mcp_proxy_adapter/core/server_adapter.py +68 -64
  73. mcp_proxy_adapter/core/server_engine.py +71 -53
  74. mcp_proxy_adapter/core/settings.py +68 -58
  75. mcp_proxy_adapter/core/ssl_utils.py +69 -56
  76. mcp_proxy_adapter/core/transport_manager.py +72 -60
  77. mcp_proxy_adapter/core/unified_config_adapter.py +201 -150
  78. mcp_proxy_adapter/core/utils.py +4 -2
  79. mcp_proxy_adapter/custom_openapi.py +107 -99
  80. mcp_proxy_adapter/examples/basic_framework/main.py +9 -2
  81. mcp_proxy_adapter/examples/commands/__init__.py +1 -1
  82. mcp_proxy_adapter/examples/create_certificates_simple.py +182 -71
  83. mcp_proxy_adapter/examples/debug_request_state.py +38 -19
  84. mcp_proxy_adapter/examples/debug_role_chain.py +53 -20
  85. mcp_proxy_adapter/examples/demo_client.py +48 -36
  86. mcp_proxy_adapter/examples/examples/basic_framework/main.py +9 -2
  87. mcp_proxy_adapter/examples/examples/full_application/__init__.py +1 -0
  88. mcp_proxy_adapter/examples/examples/full_application/commands/custom_echo_command.py +22 -10
  89. mcp_proxy_adapter/examples/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  90. mcp_proxy_adapter/examples/examples/full_application/hooks/application_hooks.py +16 -3
  91. mcp_proxy_adapter/examples/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  92. mcp_proxy_adapter/examples/examples/full_application/main.py +27 -2
  93. mcp_proxy_adapter/examples/examples/full_application/proxy_endpoints.py +48 -14
  94. mcp_proxy_adapter/examples/full_application/__init__.py +1 -0
  95. mcp_proxy_adapter/examples/full_application/commands/custom_echo_command.py +22 -10
  96. mcp_proxy_adapter/examples/full_application/commands/dynamic_calculator_command.py +24 -17
  97. mcp_proxy_adapter/examples/full_application/hooks/application_hooks.py +16 -3
  98. mcp_proxy_adapter/examples/full_application/hooks/builtin_command_hooks.py +13 -3
  99. mcp_proxy_adapter/examples/full_application/main.py +27 -2
  100. mcp_proxy_adapter/examples/full_application/proxy_endpoints.py +48 -14
  101. mcp_proxy_adapter/examples/generate_all_certificates.py +198 -73
  102. mcp_proxy_adapter/examples/generate_certificates.py +31 -16
  103. mcp_proxy_adapter/examples/generate_certificates_and_tokens.py +220 -74
  104. mcp_proxy_adapter/examples/generate_test_configs.py +68 -91
  105. mcp_proxy_adapter/examples/proxy_registration_example.py +76 -75
  106. mcp_proxy_adapter/examples/run_example.py +23 -5
  107. mcp_proxy_adapter/examples/run_full_test_suite.py +109 -71
  108. mcp_proxy_adapter/examples/run_proxy_server.py +22 -9
  109. mcp_proxy_adapter/examples/run_security_tests.py +103 -41
  110. mcp_proxy_adapter/examples/run_security_tests_fixed.py +72 -36
  111. mcp_proxy_adapter/examples/scripts/config_generator.py +288 -187
  112. mcp_proxy_adapter/examples/scripts/create_certificates_simple.py +185 -72
  113. mcp_proxy_adapter/examples/scripts/generate_certificates_and_tokens.py +220 -74
  114. mcp_proxy_adapter/examples/security_test_client.py +196 -127
  115. mcp_proxy_adapter/examples/setup_test_environment.py +17 -29
  116. mcp_proxy_adapter/examples/test_config.py +19 -4
  117. mcp_proxy_adapter/examples/test_config_generator.py +23 -7
  118. mcp_proxy_adapter/examples/test_examples.py +84 -56
  119. mcp_proxy_adapter/examples/universal_client.py +119 -62
  120. mcp_proxy_adapter/openapi.py +108 -115
  121. mcp_proxy_adapter/utils/config_generator.py +429 -274
  122. mcp_proxy_adapter/version.py +1 -2
  123. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/METADATA +1 -1
  124. mcp_proxy_adapter-6.3.5.dist-info/RECORD +143 -0
  125. mcp_proxy_adapter-6.3.4.dist-info/RECORD +0 -143
  126. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/WHEEL +0 -0
  127. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/entry_points.txt +0 -0
  128. {mcp_proxy_adapter-6.3.4.dist-info → mcp_proxy_adapter-6.3.5.dist-info}/licenses/LICENSE +0 -0
  129. {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("requests library not available, HTTP/HTTPS functionality will be limited")
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__(self, name: str, version: str, source_url: str, file_path: Optional[str] = None):
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]) -> 'CommandCatalog':
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("_load_catalog() is deprecated - catalog is always fetched fresh from servers")
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("_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]:
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(f"Invalid catalog format: expected dict, got {type(server_catalog)}")
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(f"Invalid commands format: expected list, got {type(commands_data)}")
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(f"Skipping invalid command data: expected dict, got {type(cmd_data)}")
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(f"Skipping invalid command data for {command_id}: expected dict, got {type(cmd_data)}")
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(f"Skipping command {command_id} without valid name")
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("requests library not available, cannot fetch catalog from remote server")
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(('http://', 'https://')):
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(f"Invalid catalog format from {server_url}: expected dict, got {type(server_catalog)}")
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(f"Successfully fetched catalog from {server_url}: {len(result)} commands")
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(f"Connection error while fetching catalog from {server_url}: {e}")
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(f"Unexpected error while fetching catalog from {server_url}: {e}")
344
+ logger.error(
345
+ f"Unexpected error while fetching catalog from {server_url}: {e}"
346
+ )
320
347
  return {}
321
-
322
- def _check_dependencies(self, command_name: str, server_cmd: CommandCatalog) -> bool:
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 = dependency_manager.check_dependencies(server_cmd.depends)
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(f"Command {command_name} has missing dependencies: {missing_deps}")
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(f"All dependencies satisfied for command {command_name}: {server_cmd.depends}")
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(self, command_name: str, server_cmd: CommandCatalog, auto_install: bool = None) -> bool:
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 = dependency_manager.check_dependencies(server_cmd.depends)
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(f"All dependencies already satisfied for {command_name}: {server_cmd.depends}")
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(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)}")
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(f"Installing missing dependencies for {command_name}: {missing_deps}")
379
-
380
- success, installed_deps, failed_deps = dependency_manager.install_dependencies(missing_deps)
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(f"Successfully installed all dependencies for {command_name}: {installed_deps}")
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(server_cmd.depends)
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(f"Failed to verify dependencies for {command_name}: {failed_verifications}")
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(f"Failed to install dependencies for {command_name}: {failed_deps}")
395
- logger.error(f"Please install manually: pip install {' '.join(failed_deps)}")
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(self, command_name: str, server_cmd: CommandCatalog) -> bool:
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(f"Newer version available for {command_name}: {local_ver} -> {server_ver}")
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(f"Local version {local_ver} is same or newer than server version {server_ver}")
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(self, command_name: str, server_catalog: Dict[str, CommandCatalog]) -> bool:
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("update_command() is deprecated - always downloading fresh from server (use _download_command directly)")
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("requests library not available, cannot download command from remote server")
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(f"Cannot download {command_name}: failed to install dependencies")
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(('http://', 'https://')):
479
- logger.error(f"Invalid source URL for {command_name}: {server_cmd.source_url}")
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(f"Empty response when downloading {command_name} from {server_cmd.source_url}")
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(f"Empty file content for {command_name} from {server_cmd.source_url}")
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 (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
-
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(mode='w', suffix='.py', delete=False)
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
- spec = importlib.util.spec_from_file_location(f"{command_name}_command", temp_file.name)
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, 'Command') and not any(
523
- hasattr(module, attr) and attr.endswith('Command')
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(f"Module {command_name}_command.py doesn't contain a Command class")
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 '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
-
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(f"Extracted metadata for {command_name}: {metadata}")
661
+
662
+ logger.debug(
663
+ f"Extracted metadata for {command_name}: {metadata}"
664
+ )
568
665
  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}")
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(f"Failed to clean up temporary file {temp_file.name}: {e}")
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(f"Timeout while downloading {command_name} from {server_cmd.source_url}")
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(f"Connection error while downloading {command_name} from {server_cmd.source_url}: {e}")
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(f"HTTP error while downloading {command_name} from {server_cmd.source_url}: {e}")
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(f"Request error while downloading {command_name} from {server_cmd.source_url}: {e}")
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(f"Command {command_name} already exists with same or newer version")
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(f"Found {len(commands)} local command files: {[Path(c).name for c in commands]}")
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, 'r', encoding='utf-8') as f:
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('\n')
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 line.startswith('#') or line.startswith('"""') or line.startswith("'''"):
846
+ if (
847
+ line.startswith("#")
848
+ or line.startswith('"""')
849
+ or line.startswith("'''")
850
+ ):
732
851
  # Try to parse JSON-like metadata
733
- if '{' in line and '}' in line:
852
+ if "{" in line and "}" in line:
734
853
  try:
735
854
  # Extract JSON part
736
- start = line.find('{')
737
- end = line.rfind('}') + 1
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 ['plugin:', 'descr:', 'category:', 'author:', 'version:', 'email:']:
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 '{' in docstring and '}' in docstring:
897
+ if "{" in docstring and "}" in docstring:
770
898
  try:
771
899
  # Find JSON object
772
- start = docstring.find('{')
773
- end = docstring.rfind('}') + 1
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 '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
-
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