pomera-ai-commander 1.2.4 → 1.2.7
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.
- package/README.md +23 -4
- package/core/backup_recovery_manager.py +223 -60
- package/core/collapsible_panel.py +197 -0
- package/core/database_settings_manager.py +20 -9
- package/core/migration_manager.py +12 -11
- package/core/settings_defaults_registry.py +8 -0
- package/core/tool_search_widget.py +417 -0
- package/mcp.json +1 -1
- package/package.json +2 -1
- package/pomera.py +414 -90
- package/pomera_mcp_server.py +8 -2
- package/tools/curl_history.py +45 -5
- package/tools/curl_tool.py +42 -12
- package/tools/diff_viewer.py +2 -9
- package/tools/tool_loader.py +263 -15
package/pomera_mcp_server.py
CHANGED
|
@@ -56,6 +56,12 @@ logging.basicConfig(
|
|
|
56
56
|
)
|
|
57
57
|
logger = logging.getLogger(__name__)
|
|
58
58
|
|
|
59
|
+
# Import version from unified version module
|
|
60
|
+
try:
|
|
61
|
+
from pomera.version import __version__
|
|
62
|
+
except ImportError:
|
|
63
|
+
__version__ = "unknown"
|
|
64
|
+
|
|
59
65
|
|
|
60
66
|
def main():
|
|
61
67
|
"""Main entry point for the Pomera MCP server."""
|
|
@@ -70,7 +76,7 @@ def main():
|
|
|
70
76
|
parser.add_argument(
|
|
71
77
|
"--version",
|
|
72
78
|
action="version",
|
|
73
|
-
version="pomera-mcp-server
|
|
79
|
+
version=f"pomera-mcp-server {__version__}"
|
|
74
80
|
)
|
|
75
81
|
parser.add_argument(
|
|
76
82
|
"--list-tools",
|
|
@@ -160,7 +166,7 @@ def main():
|
|
|
160
166
|
server = StdioMCPServer(
|
|
161
167
|
tool_registry=registry,
|
|
162
168
|
server_name="pomera-mcp-server",
|
|
163
|
-
server_version=
|
|
169
|
+
server_version=__version__
|
|
164
170
|
)
|
|
165
171
|
|
|
166
172
|
logger.info("Starting Pomera MCP Server...")
|
package/tools/curl_history.py
CHANGED
|
@@ -46,33 +46,39 @@ class CurlHistoryManager:
|
|
|
46
46
|
Manages request history persistence and organization for the cURL Tool.
|
|
47
47
|
|
|
48
48
|
Handles:
|
|
49
|
-
- History file storage and loading
|
|
49
|
+
- History file storage and loading (JSON or database backend)
|
|
50
50
|
- History item management (add, remove, search)
|
|
51
51
|
- History cleanup and organization
|
|
52
52
|
- Collections support
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
def __init__(self, history_file: str = "settings.json",
|
|
56
|
-
max_items: int = 100, logger=None):
|
|
56
|
+
max_items: int = 100, logger=None, db_settings_manager=None):
|
|
57
57
|
"""
|
|
58
58
|
Initialize the history manager.
|
|
59
59
|
|
|
60
60
|
Args:
|
|
61
|
-
history_file: Path to the main settings file
|
|
61
|
+
history_file: Path to the main settings file (used if no db_settings_manager)
|
|
62
62
|
max_items: Maximum number of history items to keep
|
|
63
63
|
logger: Logger instance for debugging
|
|
64
|
+
db_settings_manager: DatabaseSettingsManager instance for database backend (optional)
|
|
64
65
|
"""
|
|
65
66
|
self.history_file = history_file
|
|
66
67
|
self.max_items = max_items
|
|
67
68
|
self.logger = logger or logging.getLogger(__name__)
|
|
68
69
|
self.tool_key = "cURL Tool" # Key in tool_settings section
|
|
69
70
|
|
|
71
|
+
# Database backend support
|
|
72
|
+
self.db_settings_manager = db_settings_manager
|
|
73
|
+
self.use_database = db_settings_manager is not None
|
|
74
|
+
|
|
70
75
|
# History storage
|
|
71
76
|
self.history: List[RequestHistoryItem] = []
|
|
72
77
|
self.collections: Dict[str, List[str]] = {} # Collection name -> list of history item IDs
|
|
73
78
|
|
|
74
79
|
# Load history on initialization
|
|
75
80
|
self.load_history()
|
|
81
|
+
|
|
76
82
|
|
|
77
83
|
def add_request(self, method: str, url: str, headers: Dict[str, str] = None,
|
|
78
84
|
body: str = None, auth_type: str = "None",
|
|
@@ -505,12 +511,34 @@ class CurlHistoryManager:
|
|
|
505
511
|
|
|
506
512
|
def load_history(self) -> bool:
|
|
507
513
|
"""
|
|
508
|
-
Load history from
|
|
514
|
+
Load history from database or settings.json file.
|
|
509
515
|
|
|
510
516
|
Returns:
|
|
511
517
|
True if successful
|
|
512
518
|
"""
|
|
513
519
|
try:
|
|
520
|
+
if self.use_database and self.db_settings_manager:
|
|
521
|
+
# Load from database backend
|
|
522
|
+
curl_settings = self.db_settings_manager.get_tool_settings(self.tool_key)
|
|
523
|
+
if curl_settings:
|
|
524
|
+
# Load history items
|
|
525
|
+
self.history = []
|
|
526
|
+
for item_data in curl_settings.get("history", []):
|
|
527
|
+
try:
|
|
528
|
+
item = RequestHistoryItem.from_dict(item_data)
|
|
529
|
+
self.history.append(item)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
self.logger.warning(f"Error loading history item: {e}")
|
|
532
|
+
|
|
533
|
+
# Load collections
|
|
534
|
+
self.collections = curl_settings.get("collections", {})
|
|
535
|
+
|
|
536
|
+
self.logger.info(f"Loaded {len(self.history)} history items from database")
|
|
537
|
+
else:
|
|
538
|
+
self.logger.info("No history found in database, starting with empty history")
|
|
539
|
+
return True
|
|
540
|
+
|
|
541
|
+
# Fallback to JSON file
|
|
514
542
|
if os.path.exists(self.history_file):
|
|
515
543
|
with open(self.history_file, 'r', encoding='utf-8') as f:
|
|
516
544
|
all_settings = json.load(f)
|
|
@@ -543,12 +571,24 @@ class CurlHistoryManager:
|
|
|
543
571
|
|
|
544
572
|
def save_history(self) -> bool:
|
|
545
573
|
"""
|
|
546
|
-
Save history to
|
|
574
|
+
Save history to database or settings.json file.
|
|
547
575
|
|
|
548
576
|
Returns:
|
|
549
577
|
True if successful
|
|
550
578
|
"""
|
|
551
579
|
try:
|
|
580
|
+
if self.use_database and self.db_settings_manager:
|
|
581
|
+
# Save to database backend
|
|
582
|
+
history_data = [item.to_dict() for item in self.history]
|
|
583
|
+
self.db_settings_manager.set_tool_setting(self.tool_key, "history", history_data)
|
|
584
|
+
self.db_settings_manager.set_tool_setting(self.tool_key, "collections", self.collections)
|
|
585
|
+
self.db_settings_manager.set_tool_setting(self.tool_key, "history_last_updated", datetime.now().isoformat())
|
|
586
|
+
self.db_settings_manager.set_tool_setting(self.tool_key, "history_version", "1.0")
|
|
587
|
+
|
|
588
|
+
self.logger.debug("History saved to database")
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
# Fallback to JSON file
|
|
552
592
|
# Load existing settings file
|
|
553
593
|
all_settings = {}
|
|
554
594
|
if os.path.exists(self.history_file):
|
package/tools/curl_tool.py
CHANGED
|
@@ -60,6 +60,17 @@ except ImportError:
|
|
|
60
60
|
CURL_HISTORY_AVAILABLE = False
|
|
61
61
|
print("cURL modules not available")
|
|
62
62
|
|
|
63
|
+
# Import database-compatible settings manager
|
|
64
|
+
try:
|
|
65
|
+
from core.database_curl_settings_manager import DatabaseCurlSettingsManager
|
|
66
|
+
DATABASE_CURL_SETTINGS_AVAILABLE = True
|
|
67
|
+
except ImportError:
|
|
68
|
+
try:
|
|
69
|
+
from ..core.database_curl_settings_manager import DatabaseCurlSettingsManager
|
|
70
|
+
DATABASE_CURL_SETTINGS_AVAILABLE = True
|
|
71
|
+
except ImportError:
|
|
72
|
+
DATABASE_CURL_SETTINGS_AVAILABLE = False
|
|
73
|
+
|
|
63
74
|
|
|
64
75
|
def get_system_encryption_key():
|
|
65
76
|
"""Generate encryption key based on system characteristics (same as AI Tools)"""
|
|
@@ -138,7 +149,7 @@ class CurlToolWidget:
|
|
|
138
149
|
with the application's tool ecosystem.
|
|
139
150
|
"""
|
|
140
151
|
|
|
141
|
-
def __init__(self, parent, logger=None, send_to_input_callback=None, dialog_manager=None):
|
|
152
|
+
def __init__(self, parent, logger=None, send_to_input_callback=None, dialog_manager=None, db_settings_manager=None):
|
|
142
153
|
"""
|
|
143
154
|
Initialize the cURL Tool widget.
|
|
144
155
|
|
|
@@ -147,21 +158,30 @@ class CurlToolWidget:
|
|
|
147
158
|
logger: Logger instance for debugging
|
|
148
159
|
send_to_input_callback: Callback function to send content to input tabs
|
|
149
160
|
dialog_manager: DialogManager instance for configurable dialogs
|
|
161
|
+
db_settings_manager: DatabaseSettingsManager instance for database backend (optional)
|
|
150
162
|
"""
|
|
151
163
|
self.parent = parent
|
|
152
164
|
self.logger = logger or logging.getLogger(__name__)
|
|
153
165
|
self.send_to_input_callback = send_to_input_callback
|
|
154
166
|
self.dialog_manager = dialog_manager
|
|
167
|
+
self.db_settings_manager = db_settings_manager # Store for database backend
|
|
155
168
|
|
|
156
|
-
# Initialize processor
|
|
169
|
+
# Initialize processor
|
|
157
170
|
if CURL_PROCESSOR_AVAILABLE:
|
|
158
171
|
self.processor = CurlProcessor()
|
|
159
172
|
else:
|
|
160
173
|
self.processor = None
|
|
161
174
|
self.logger.error("cURL Processor not available")
|
|
162
175
|
|
|
163
|
-
if
|
|
176
|
+
# Initialize settings manager - prefer database backend if available
|
|
177
|
+
if db_settings_manager and DATABASE_CURL_SETTINGS_AVAILABLE:
|
|
178
|
+
# Use database-backed settings manager
|
|
179
|
+
self.settings_manager = DatabaseCurlSettingsManager(db_settings_manager, logger=self.logger)
|
|
180
|
+
self.logger.info("cURL Tool using database backend for settings")
|
|
181
|
+
elif CURL_SETTINGS_AVAILABLE:
|
|
182
|
+
# Fallback to JSON-based settings manager
|
|
164
183
|
self.settings_manager = CurlSettingsManager(logger=self.logger)
|
|
184
|
+
self.logger.info("cURL Tool using JSON backend for settings (database not available)")
|
|
165
185
|
else:
|
|
166
186
|
self.settings_manager = None
|
|
167
187
|
self.logger.error("cURL Settings Manager not available")
|
|
@@ -174,7 +194,7 @@ class CurlToolWidget:
|
|
|
174
194
|
|
|
175
195
|
# Settings - load from settings manager or use defaults (must be loaded before history manager)
|
|
176
196
|
if self.settings_manager:
|
|
177
|
-
self.settings = self.settings_manager.
|
|
197
|
+
self.settings = self.settings_manager.load_settings()
|
|
178
198
|
else:
|
|
179
199
|
self.settings = {
|
|
180
200
|
"timeout": 30,
|
|
@@ -190,16 +210,26 @@ class CurlToolWidget:
|
|
|
190
210
|
"history_retention_days": 30
|
|
191
211
|
}
|
|
192
212
|
|
|
213
|
+
# Initialize history manager - use database backend if available
|
|
193
214
|
if CURL_HISTORY_AVAILABLE:
|
|
194
215
|
max_history = self.settings.get("max_history_items", 100)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
if db_settings_manager and DATABASE_CURL_SETTINGS_AVAILABLE:
|
|
217
|
+
# Use database backend for history (through DatabaseCurlSettingsManager)
|
|
218
|
+
self.history_manager = CurlHistoryManager(
|
|
219
|
+
max_items=max_history,
|
|
220
|
+
logger=self.logger,
|
|
221
|
+
db_settings_manager=db_settings_manager
|
|
222
|
+
)
|
|
223
|
+
self.logger.debug("History manager initialized with database backend")
|
|
224
|
+
else:
|
|
225
|
+
# Fallback to JSON file
|
|
226
|
+
history_file = os.path.abspath("settings.json")
|
|
227
|
+
self.history_manager = CurlHistoryManager(
|
|
228
|
+
history_file=history_file,
|
|
229
|
+
max_items=max_history,
|
|
230
|
+
logger=self.logger
|
|
231
|
+
)
|
|
232
|
+
self.logger.debug(f"History manager initialized with file: {history_file}")
|
|
203
233
|
else:
|
|
204
234
|
self.history_manager = None
|
|
205
235
|
self.logger.error("cURL History Manager not available")
|
package/tools/diff_viewer.py
CHANGED
|
@@ -458,7 +458,8 @@ class DiffViewerWidget:
|
|
|
458
458
|
|
|
459
459
|
def show(self):
|
|
460
460
|
"""Show the diff viewer."""
|
|
461
|
-
|
|
461
|
+
# Use row=1 (same as central_frame) to not cover search bar in row=0
|
|
462
|
+
self.diff_frame.grid(row=1, column=0, sticky="nsew", pady=5)
|
|
462
463
|
|
|
463
464
|
def hide(self):
|
|
464
465
|
"""Hide the diff viewer."""
|
|
@@ -1658,14 +1659,6 @@ class DiffViewerSettingsWidget:
|
|
|
1658
1659
|
variable=self.syntax_var,
|
|
1659
1660
|
command=self._on_syntax_change
|
|
1660
1661
|
).pack(side=tk.LEFT, padx=(0, 8))
|
|
1661
|
-
|
|
1662
|
-
ttk.Label(row2, text="|").pack(side=tk.LEFT, padx=5)
|
|
1663
|
-
|
|
1664
|
-
ttk.Button(
|
|
1665
|
-
row2,
|
|
1666
|
-
text="List Comparator",
|
|
1667
|
-
command=self._launch_list_comparator
|
|
1668
|
-
).pack(side=tk.LEFT, padx=5)
|
|
1669
1662
|
|
|
1670
1663
|
def _on_option_change(self):
|
|
1671
1664
|
"""Handle option change."""
|
package/tools/tool_loader.py
CHANGED
|
@@ -9,10 +9,20 @@ Author: Pomera AI Commander Team
|
|
|
9
9
|
|
|
10
10
|
import importlib
|
|
11
11
|
import logging
|
|
12
|
+
import threading
|
|
12
13
|
from typing import Dict, Any, Optional, Callable, Type, List, Tuple
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from enum import Enum
|
|
15
16
|
|
|
17
|
+
# Optional rapidfuzz for fuzzy search
|
|
18
|
+
try:
|
|
19
|
+
from rapidfuzz import fuzz, process
|
|
20
|
+
RAPIDFUZZ_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
fuzz = None
|
|
23
|
+
process = None
|
|
24
|
+
RAPIDFUZZ_AVAILABLE = False
|
|
25
|
+
|
|
16
26
|
|
|
17
27
|
logger = logging.getLogger(__name__)
|
|
18
28
|
|
|
@@ -68,12 +78,11 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
68
78
|
description="Transform text case (uppercase, lowercase, title case, etc.)",
|
|
69
79
|
available_flag="CASE_TOOL_MODULE_AVAILABLE"
|
|
70
80
|
),
|
|
71
|
-
"Find & Replace": ToolSpec(
|
|
72
|
-
name="Find & Replace",
|
|
81
|
+
"Find & Replace Text": ToolSpec(
|
|
82
|
+
name="Find & Replace Text",
|
|
73
83
|
module_path="tools.find_replace",
|
|
74
84
|
class_name="FindReplaceWidget",
|
|
75
85
|
category=ToolCategory.CORE,
|
|
76
|
-
is_widget=True,
|
|
77
86
|
description="Find and replace text with regex support",
|
|
78
87
|
available_flag="FIND_REPLACE_MODULE_AVAILABLE"
|
|
79
88
|
),
|
|
@@ -83,7 +92,6 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
83
92
|
class_name="DiffViewerWidget",
|
|
84
93
|
widget_class="DiffViewerSettingsWidget",
|
|
85
94
|
category=ToolCategory.CORE,
|
|
86
|
-
is_widget=True,
|
|
87
95
|
description="Compare and view differences between texts",
|
|
88
96
|
available_flag="DIFF_VIEWER_MODULE_AVAILABLE"
|
|
89
97
|
),
|
|
@@ -94,7 +102,6 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
94
102
|
module_path="tools.ai_tools",
|
|
95
103
|
class_name="AIToolsWidget",
|
|
96
104
|
category=ToolCategory.AI,
|
|
97
|
-
is_widget=True,
|
|
98
105
|
description="AI-powered text processing with multiple providers",
|
|
99
106
|
available_flag="AI_TOOLS_AVAILABLE"
|
|
100
107
|
),
|
|
@@ -158,8 +165,8 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
158
165
|
),
|
|
159
166
|
|
|
160
167
|
# Conversion Tools
|
|
161
|
-
"Base64
|
|
162
|
-
name="Base64
|
|
168
|
+
"Base64 Encoder/Decoder": ToolSpec(
|
|
169
|
+
name="Base64 Encoder/Decoder",
|
|
163
170
|
module_path="tools.base64_tools",
|
|
164
171
|
class_name="Base64Tools",
|
|
165
172
|
widget_class="Base64ToolsWidget",
|
|
@@ -334,6 +341,7 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
334
341
|
module_path="tools.list_comparator",
|
|
335
342
|
class_name="DiffApp",
|
|
336
343
|
category=ToolCategory.UTILITY,
|
|
344
|
+
is_widget=True, # Exclude from search - standalone widget
|
|
337
345
|
description="Compare two lists and find differences",
|
|
338
346
|
available_flag="LIST_COMPARATOR_MODULE_AVAILABLE"
|
|
339
347
|
),
|
|
@@ -367,6 +375,89 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
367
375
|
),
|
|
368
376
|
}
|
|
369
377
|
|
|
378
|
+
# These sub-tools appear as tabs within their parent tool
|
|
379
|
+
PARENT_TOOLS = {
|
|
380
|
+
"AI Tools": [
|
|
381
|
+
# Tab order from UI screenshot
|
|
382
|
+
"Google AI",
|
|
383
|
+
"Vertex AI",
|
|
384
|
+
"Azure AI",
|
|
385
|
+
"Anthropic AI",
|
|
386
|
+
"OpenAI",
|
|
387
|
+
"Cohere AI",
|
|
388
|
+
"HuggingFace AI",
|
|
389
|
+
"Groq AI",
|
|
390
|
+
"OpenRouterAI",
|
|
391
|
+
"LM Studio",
|
|
392
|
+
"AWS Bedrock",
|
|
393
|
+
],
|
|
394
|
+
"Extraction Tools": [
|
|
395
|
+
"Email Extraction",
|
|
396
|
+
"HTML Tool",
|
|
397
|
+
"Regex Extractor",
|
|
398
|
+
"URL Link Extractor",
|
|
399
|
+
],
|
|
400
|
+
"Generator Tools": [
|
|
401
|
+
# Tab order: Strong Password | Repeating Text | Lorem Ipsum | UUID/GUID | Random Email | ASCII Art | Hash | Slug
|
|
402
|
+
"Strong Password Generator",
|
|
403
|
+
"Repeating Text Generator",
|
|
404
|
+
"Lorem Ipsum Generator",
|
|
405
|
+
"UUID/GUID Generator",
|
|
406
|
+
"Random Email Generator",
|
|
407
|
+
"ASCII Art Generator",
|
|
408
|
+
"Hash Generator",
|
|
409
|
+
"Slug Generator",
|
|
410
|
+
],
|
|
411
|
+
"Line Tools": [
|
|
412
|
+
# Tab order from line_tools.py
|
|
413
|
+
"Remove Duplicates",
|
|
414
|
+
"Remove Empty Lines",
|
|
415
|
+
"Add Line Numbers",
|
|
416
|
+
"Remove Line Numbers",
|
|
417
|
+
"Reverse Lines",
|
|
418
|
+
"Shuffle Lines",
|
|
419
|
+
],
|
|
420
|
+
"Markdown Tools": [
|
|
421
|
+
# Tab order from markdown_tools.py
|
|
422
|
+
"Strip Markdown",
|
|
423
|
+
"Extract Links",
|
|
424
|
+
"Extract Headers",
|
|
425
|
+
"Table to CSV",
|
|
426
|
+
"Format Table",
|
|
427
|
+
],
|
|
428
|
+
"Sorter Tools": [
|
|
429
|
+
# Tab order from sorter_tools.py
|
|
430
|
+
"Number Sorter",
|
|
431
|
+
"Alphabetical Sorter",
|
|
432
|
+
],
|
|
433
|
+
"Text Wrapper": [
|
|
434
|
+
# Tab order from text_wrapper.py
|
|
435
|
+
"Word Wrap",
|
|
436
|
+
"Justify Text",
|
|
437
|
+
"Prefix/Suffix",
|
|
438
|
+
"Indent Text",
|
|
439
|
+
"Quote Text",
|
|
440
|
+
],
|
|
441
|
+
"Translator Tools": [
|
|
442
|
+
# Tab order from translator_tools.py
|
|
443
|
+
"Morse Code Translator",
|
|
444
|
+
"Binary Code Translator",
|
|
445
|
+
],
|
|
446
|
+
"Whitespace Tools": [
|
|
447
|
+
# Tab order from whitespace_tools.py
|
|
448
|
+
"Trim Lines",
|
|
449
|
+
"Remove Extra Spaces",
|
|
450
|
+
"Tabs/Spaces Converter",
|
|
451
|
+
"Normalize Line Endings",
|
|
452
|
+
],
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Reverse lookup: sub-tool -> parent
|
|
456
|
+
SUB_TOOL_PARENTS = {
|
|
457
|
+
sub: parent
|
|
458
|
+
for parent, subs in PARENT_TOOLS.items()
|
|
459
|
+
for sub in subs
|
|
460
|
+
}
|
|
370
461
|
|
|
371
462
|
class ToolLoader:
|
|
372
463
|
"""
|
|
@@ -588,6 +679,75 @@ class ToolLoader:
|
|
|
588
679
|
if spec.category == category and self.is_available(name)
|
|
589
680
|
]
|
|
590
681
|
|
|
682
|
+
def get_processing_tools(self) -> List[str]:
|
|
683
|
+
"""
|
|
684
|
+
Get list of available text processing tools (excludes standalone widgets).
|
|
685
|
+
|
|
686
|
+
This filters out tools like Notes Widget, MCP Manager that are
|
|
687
|
+
standalone windows rather than text processing tools.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
List of processing tool names
|
|
691
|
+
"""
|
|
692
|
+
return [
|
|
693
|
+
name for name, spec in self._specs.items()
|
|
694
|
+
if self.is_available(name) and not spec.is_widget
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
def get_processing_tools_by_category(self, category: ToolCategory) -> List[str]:
|
|
698
|
+
"""
|
|
699
|
+
Get processing tools in a specific category (excludes standalone widgets).
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
category: Tool category
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
List of processing tool names in that category
|
|
706
|
+
"""
|
|
707
|
+
return [
|
|
708
|
+
name for name, spec in self._specs.items()
|
|
709
|
+
if spec.category == category and self.is_available(name) and not spec.is_widget
|
|
710
|
+
]
|
|
711
|
+
|
|
712
|
+
def get_grouped_tools(self) -> List[Tuple[str, bool]]:
|
|
713
|
+
"""
|
|
714
|
+
Get processing tools grouped with parent-child relationships.
|
|
715
|
+
|
|
716
|
+
Returns tools sorted alphabetically, with sub-tools appearing
|
|
717
|
+
immediately after their parent tool.
|
|
718
|
+
|
|
719
|
+
Returns:
|
|
720
|
+
List of tuples: (tool_name, is_sub_tool)
|
|
721
|
+
is_sub_tool is True for tools that belong under a parent
|
|
722
|
+
"""
|
|
723
|
+
# Get all processing tools (excludes widgets)
|
|
724
|
+
all_tools = set(self.get_processing_tools())
|
|
725
|
+
|
|
726
|
+
# Build result with grouping
|
|
727
|
+
result: List[Tuple[str, bool]] = []
|
|
728
|
+
processed = set()
|
|
729
|
+
|
|
730
|
+
# Sort main tools alphabetically
|
|
731
|
+
main_tools = sorted([t for t in all_tools if t not in SUB_TOOL_PARENTS])
|
|
732
|
+
|
|
733
|
+
for tool in main_tools:
|
|
734
|
+
if tool in processed:
|
|
735
|
+
continue
|
|
736
|
+
|
|
737
|
+
# Add the main tool
|
|
738
|
+
result.append((tool, False))
|
|
739
|
+
processed.add(tool)
|
|
740
|
+
|
|
741
|
+
# If this is a parent, add all its children (they're virtual sub-tools for tabs)
|
|
742
|
+
if tool in PARENT_TOOLS:
|
|
743
|
+
children = PARENT_TOOLS[tool]
|
|
744
|
+
for child in children:
|
|
745
|
+
if child not in processed:
|
|
746
|
+
result.append((child, True))
|
|
747
|
+
processed.add(child)
|
|
748
|
+
|
|
749
|
+
return result
|
|
750
|
+
|
|
591
751
|
def get_tool_spec(self, tool_name: str) -> Optional[ToolSpec]:
|
|
592
752
|
"""
|
|
593
753
|
Get the specification for a tool.
|
|
@@ -657,6 +817,86 @@ class ToolLoader:
|
|
|
657
817
|
self._widget_classes.clear()
|
|
658
818
|
logger.debug("Tool loader cache cleared")
|
|
659
819
|
|
|
820
|
+
def search_tools(
|
|
821
|
+
self,
|
|
822
|
+
query: str,
|
|
823
|
+
limit: int = 10,
|
|
824
|
+
include_unavailable: bool = False
|
|
825
|
+
) -> List[Tuple[str, int, 'ToolCategory']]:
|
|
826
|
+
"""
|
|
827
|
+
Fuzzy search tools by name and description.
|
|
828
|
+
|
|
829
|
+
Uses rapidfuzz for fuzzy matching if available, otherwise falls back
|
|
830
|
+
to simple substring matching.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
query: Search query string
|
|
834
|
+
limit: Maximum number of results to return
|
|
835
|
+
include_unavailable: Whether to include unavailable tools
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
List of tuples: (tool_name, match_score, category)
|
|
839
|
+
Score is 0-100, higher is better match
|
|
840
|
+
"""
|
|
841
|
+
if not query:
|
|
842
|
+
# Return all tools sorted alphabetically
|
|
843
|
+
tools = self.get_available_tools() if not include_unavailable else list(self._specs.keys())
|
|
844
|
+
return [(name, 100, self._specs[name].category) for name in sorted(tools)[:limit]]
|
|
845
|
+
|
|
846
|
+
# Build search data: name -> searchable text
|
|
847
|
+
search_data = {}
|
|
848
|
+
for name, spec in self._specs.items():
|
|
849
|
+
if not include_unavailable and not self.is_available(name):
|
|
850
|
+
continue
|
|
851
|
+
# Combine name and description for searching
|
|
852
|
+
search_text = f"{name} {spec.description}"
|
|
853
|
+
search_data[name] = search_text
|
|
854
|
+
|
|
855
|
+
if not search_data:
|
|
856
|
+
return []
|
|
857
|
+
|
|
858
|
+
results: List[Tuple[str, int, ToolCategory]] = []
|
|
859
|
+
|
|
860
|
+
if RAPIDFUZZ_AVAILABLE and process is not None:
|
|
861
|
+
# Use rapidfuzz for proper fuzzy matching
|
|
862
|
+
matches = process.extract(
|
|
863
|
+
query,
|
|
864
|
+
search_data,
|
|
865
|
+
scorer=fuzz.WRatio,
|
|
866
|
+
limit=limit
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
for match in matches:
|
|
870
|
+
tool_name = match[2] # Key from dict
|
|
871
|
+
score = int(match[1]) # Score 0-100
|
|
872
|
+
if score >= 30: # Minimum relevance threshold
|
|
873
|
+
category = self._specs[tool_name].category
|
|
874
|
+
results.append((tool_name, score, category))
|
|
875
|
+
else:
|
|
876
|
+
# Fallback to simple substring matching
|
|
877
|
+
query_lower = query.lower()
|
|
878
|
+
for name, search_text in search_data.items():
|
|
879
|
+
text_lower = search_text.lower()
|
|
880
|
+
if query_lower in text_lower:
|
|
881
|
+
# Score based on position and exact match
|
|
882
|
+
if name.lower() == query_lower:
|
|
883
|
+
score = 100
|
|
884
|
+
elif name.lower().startswith(query_lower):
|
|
885
|
+
score = 90
|
|
886
|
+
elif query_lower in name.lower():
|
|
887
|
+
score = 80
|
|
888
|
+
else:
|
|
889
|
+
score = 60 # Match in description only
|
|
890
|
+
|
|
891
|
+
category = self._specs[name].category
|
|
892
|
+
results.append((name, score, category))
|
|
893
|
+
|
|
894
|
+
# Sort by score descending, then name ascending
|
|
895
|
+
results.sort(key=lambda x: (-x[1], x[0]))
|
|
896
|
+
results = results[:limit]
|
|
897
|
+
|
|
898
|
+
return results
|
|
899
|
+
|
|
660
900
|
def get_legacy_flags(self) -> Dict[str, bool]:
|
|
661
901
|
"""
|
|
662
902
|
Get legacy availability flags for backwards compatibility.
|
|
@@ -671,26 +911,33 @@ class ToolLoader:
|
|
|
671
911
|
return flags
|
|
672
912
|
|
|
673
913
|
|
|
674
|
-
# Global instance
|
|
914
|
+
# Global instance with thread-safe initialization
|
|
675
915
|
_tool_loader: Optional[ToolLoader] = None
|
|
916
|
+
_tool_loader_lock = threading.Lock()
|
|
676
917
|
|
|
677
918
|
|
|
678
919
|
def get_tool_loader() -> ToolLoader:
|
|
679
920
|
"""
|
|
680
|
-
Get the global tool loader instance.
|
|
921
|
+
Get the global tool loader instance (thread-safe).
|
|
922
|
+
|
|
923
|
+
Uses double-checked locking pattern to ensure thread safety
|
|
924
|
+
while minimizing lock contention after initialization.
|
|
681
925
|
|
|
682
926
|
Returns:
|
|
683
927
|
Global ToolLoader instance
|
|
684
928
|
"""
|
|
685
929
|
global _tool_loader
|
|
686
930
|
if _tool_loader is None:
|
|
687
|
-
|
|
931
|
+
with _tool_loader_lock:
|
|
932
|
+
# Double-check after acquiring lock
|
|
933
|
+
if _tool_loader is None:
|
|
934
|
+
_tool_loader = ToolLoader()
|
|
688
935
|
return _tool_loader
|
|
689
936
|
|
|
690
937
|
|
|
691
938
|
def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLoader:
|
|
692
939
|
"""
|
|
693
|
-
Initialize the global tool loader.
|
|
940
|
+
Initialize the global tool loader (thread-safe).
|
|
694
941
|
|
|
695
942
|
Args:
|
|
696
943
|
tool_specs: Optional custom tool specifications
|
|
@@ -699,12 +946,13 @@ def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLo
|
|
|
699
946
|
Initialized ToolLoader
|
|
700
947
|
"""
|
|
701
948
|
global _tool_loader
|
|
702
|
-
|
|
949
|
+
with _tool_loader_lock:
|
|
950
|
+
_tool_loader = ToolLoader(tool_specs)
|
|
703
951
|
return _tool_loader
|
|
704
952
|
|
|
705
953
|
|
|
706
954
|
def reset_tool_loader() -> None:
|
|
707
|
-
"""Reset the global tool loader."""
|
|
955
|
+
"""Reset the global tool loader (thread-safe)."""
|
|
708
956
|
global _tool_loader
|
|
709
|
-
|
|
710
|
-
|
|
957
|
+
with _tool_loader_lock:
|
|
958
|
+
_tool_loader = None
|