pomera-ai-commander 1.2.5 → 1.2.8

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.
@@ -53,9 +53,10 @@ class Base64Tools:
53
53
  class Base64ToolsWidget:
54
54
  """Widget for the Base64 Tools interface."""
55
55
 
56
- def __init__(self, base64_tools):
56
+ def __init__(self, base64_tools=None):
57
57
  """Initialize the Base64ToolsWidget."""
58
- self.base64_tools = base64_tools
58
+ # Create Base64Tools instance if not provided
59
+ self.base64_tools = base64_tools if base64_tools else Base64Tools()
59
60
  self.main_app = None
60
61
 
61
62
  # Variables for Base64 mode
@@ -102,8 +103,7 @@ class Base64ToolsWidget:
102
103
  def on_mode_change(self):
103
104
  """Handle mode change and save settings."""
104
105
  self.save_settings()
105
- # Auto-process if there's input text
106
- self.process_base64()
106
+ # Don't auto-process - wait for Process button click
107
107
 
108
108
  def process_base64(self):
109
109
  """Process the input text with Base64 encoding/decoding."""
@@ -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 centralized settings.json file.
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 centralized settings.json file.
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):
@@ -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 and settings manager
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 CURL_SETTINGS_AVAILABLE:
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.get_all_settings()
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
- # Use centralized settings.json file
196
- history_file = os.path.abspath("settings.json")
197
- self.history_manager = CurlHistoryManager(
198
- history_file=history_file,
199
- max_items=max_history,
200
- logger=self.logger
201
- )
202
- self.logger.debug(f"History manager initialized with file: {history_file}")
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")
@@ -458,7 +458,8 @@ class DiffViewerWidget:
458
458
 
459
459
  def show(self):
460
460
  """Show the diff viewer."""
461
- self.diff_frame.grid(row=0, column=0, sticky="nsew", pady=5)
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."""
@@ -716,9 +716,16 @@ class NotesWidget:
716
716
  • Search specific columns: Title:refactor OR Input:code.
717
717
  • Leave empty to show all records."""
718
718
  if self.dialog_manager:
719
- self.dialog_manager.show_info("Search Help", help_text)
719
+ self.dialog_manager.show_info("Search Help", help_text, parent=self.parent)
720
720
  else:
721
721
  messagebox.showinfo("Search Help", help_text, parent=self.parent)
722
+
723
+ # Return focus to Notes window after dialog closes
724
+ try:
725
+ self.parent.focus_force()
726
+ self.search_entry.focus_set()
727
+ except Exception:
728
+ pass # Widget may not exist
722
729
 
723
730
  def new_note(self) -> None:
724
731
  """Create a new note."""
@@ -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 Tools": ToolSpec(
162
- name="Base64 Tools",
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",
@@ -294,20 +301,13 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
294
301
  ),
295
302
 
296
303
  # Analysis Tools
297
- "Word Frequency Counter": ToolSpec(
298
- name="Word Frequency Counter",
299
- module_path="tools.word_frequency_counter",
300
- class_name="WordFrequencyCounter",
301
- category=ToolCategory.ANALYSIS,
302
- description="Count word frequencies in text",
303
- available_flag="WORD_FREQUENCY_COUNTER_MODULE_AVAILABLE"
304
- ),
304
+ # NOTE: Word Frequency Counter merged into Text Statistics (has "Word Frequency Counter" button)
305
305
  "Text Statistics": ToolSpec(
306
306
  name="Text Statistics",
307
307
  module_path="tools.text_statistics_tool",
308
308
  class_name="TextStatistics",
309
309
  category=ToolCategory.ANALYSIS,
310
- description="Calculate text statistics (chars, words, lines)",
310
+ description="Text stats, character/word/line counts, word frequency",
311
311
  available_flag="TEXT_STATISTICS_MODULE_AVAILABLE"
312
312
  ),
313
313
  "Cron Tool": ToolSpec(
@@ -334,6 +334,7 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
334
334
  module_path="tools.list_comparator",
335
335
  class_name="DiffApp",
336
336
  category=ToolCategory.UTILITY,
337
+ is_widget=True, # Exclude from search - standalone widget
337
338
  description="Compare two lists and find differences",
338
339
  available_flag="LIST_COMPARATOR_MODULE_AVAILABLE"
339
340
  ),
@@ -367,6 +368,89 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
367
368
  ),
368
369
  }
369
370
 
371
+ # These sub-tools appear as tabs within their parent tool
372
+ PARENT_TOOLS = {
373
+ "AI Tools": [
374
+ # Tab order from UI screenshot
375
+ "Google AI",
376
+ "Vertex AI",
377
+ "Azure AI",
378
+ "Anthropic AI",
379
+ "OpenAI",
380
+ "Cohere AI",
381
+ "HuggingFace AI",
382
+ "Groq AI",
383
+ "OpenRouterAI",
384
+ "LM Studio",
385
+ "AWS Bedrock",
386
+ ],
387
+ "Extraction Tools": [
388
+ "Email Extraction",
389
+ "HTML Tool",
390
+ "Regex Extractor",
391
+ "URL Link Extractor",
392
+ ],
393
+ "Generator Tools": [
394
+ # Tab order: Strong Password | Repeating Text | Lorem Ipsum | UUID/GUID | Random Email | ASCII Art | Hash | Slug
395
+ "Strong Password Generator",
396
+ "Repeating Text Generator",
397
+ "Lorem Ipsum Generator",
398
+ "UUID/GUID Generator",
399
+ "Random Email Generator",
400
+ "ASCII Art Generator",
401
+ "Hash Generator",
402
+ "Slug Generator",
403
+ ],
404
+ "Line Tools": [
405
+ # Tab order from line_tools.py
406
+ "Remove Duplicates",
407
+ "Remove Empty Lines",
408
+ "Add Line Numbers",
409
+ "Remove Line Numbers",
410
+ "Reverse Lines",
411
+ "Shuffle Lines",
412
+ ],
413
+ "Markdown Tools": [
414
+ # Tab order from markdown_tools.py
415
+ "Strip Markdown",
416
+ "Extract Links",
417
+ "Extract Headers",
418
+ "Table to CSV",
419
+ "Format Table",
420
+ ],
421
+ "Sorter Tools": [
422
+ # Tab order from sorter_tools.py
423
+ "Number Sorter",
424
+ "Alphabetical Sorter",
425
+ ],
426
+ "Text Wrapper": [
427
+ # Tab order from text_wrapper.py
428
+ "Word Wrap",
429
+ "Justify Text",
430
+ "Prefix/Suffix",
431
+ "Indent Text",
432
+ "Quote Text",
433
+ ],
434
+ "Translator Tools": [
435
+ # Tab order from translator_tools.py
436
+ "Morse Code Translator",
437
+ "Binary Code Translator",
438
+ ],
439
+ "Whitespace Tools": [
440
+ # Tab order from whitespace_tools.py
441
+ "Trim Lines",
442
+ "Remove Extra Spaces",
443
+ "Tabs/Spaces Converter",
444
+ "Normalize Line Endings",
445
+ ],
446
+ }
447
+
448
+ # Reverse lookup: sub-tool -> parent
449
+ SUB_TOOL_PARENTS = {
450
+ sub: parent
451
+ for parent, subs in PARENT_TOOLS.items()
452
+ for sub in subs
453
+ }
370
454
 
371
455
  class ToolLoader:
372
456
  """
@@ -588,6 +672,75 @@ class ToolLoader:
588
672
  if spec.category == category and self.is_available(name)
589
673
  ]
590
674
 
675
+ def get_processing_tools(self) -> List[str]:
676
+ """
677
+ Get list of available text processing tools (excludes standalone widgets).
678
+
679
+ This filters out tools like Notes Widget, MCP Manager that are
680
+ standalone windows rather than text processing tools.
681
+
682
+ Returns:
683
+ List of processing tool names
684
+ """
685
+ return [
686
+ name for name, spec in self._specs.items()
687
+ if self.is_available(name) and not spec.is_widget
688
+ ]
689
+
690
+ def get_processing_tools_by_category(self, category: ToolCategory) -> List[str]:
691
+ """
692
+ Get processing tools in a specific category (excludes standalone widgets).
693
+
694
+ Args:
695
+ category: Tool category
696
+
697
+ Returns:
698
+ List of processing tool names in that category
699
+ """
700
+ return [
701
+ name for name, spec in self._specs.items()
702
+ if spec.category == category and self.is_available(name) and not spec.is_widget
703
+ ]
704
+
705
+ def get_grouped_tools(self) -> List[Tuple[str, bool]]:
706
+ """
707
+ Get processing tools grouped with parent-child relationships.
708
+
709
+ Returns tools sorted alphabetically, with sub-tools appearing
710
+ immediately after their parent tool.
711
+
712
+ Returns:
713
+ List of tuples: (tool_name, is_sub_tool)
714
+ is_sub_tool is True for tools that belong under a parent
715
+ """
716
+ # Get all processing tools (excludes widgets)
717
+ all_tools = set(self.get_processing_tools())
718
+
719
+ # Build result with grouping
720
+ result: List[Tuple[str, bool]] = []
721
+ processed = set()
722
+
723
+ # Sort main tools alphabetically
724
+ main_tools = sorted([t for t in all_tools if t not in SUB_TOOL_PARENTS])
725
+
726
+ for tool in main_tools:
727
+ if tool in processed:
728
+ continue
729
+
730
+ # Add the main tool
731
+ result.append((tool, False))
732
+ processed.add(tool)
733
+
734
+ # If this is a parent, add all its children (they're virtual sub-tools for tabs)
735
+ if tool in PARENT_TOOLS:
736
+ children = PARENT_TOOLS[tool]
737
+ for child in children:
738
+ if child not in processed:
739
+ result.append((child, True))
740
+ processed.add(child)
741
+
742
+ return result
743
+
591
744
  def get_tool_spec(self, tool_name: str) -> Optional[ToolSpec]:
592
745
  """
593
746
  Get the specification for a tool.
@@ -657,6 +810,86 @@ class ToolLoader:
657
810
  self._widget_classes.clear()
658
811
  logger.debug("Tool loader cache cleared")
659
812
 
813
+ def search_tools(
814
+ self,
815
+ query: str,
816
+ limit: int = 10,
817
+ include_unavailable: bool = False
818
+ ) -> List[Tuple[str, int, 'ToolCategory']]:
819
+ """
820
+ Fuzzy search tools by name and description.
821
+
822
+ Uses rapidfuzz for fuzzy matching if available, otherwise falls back
823
+ to simple substring matching.
824
+
825
+ Args:
826
+ query: Search query string
827
+ limit: Maximum number of results to return
828
+ include_unavailable: Whether to include unavailable tools
829
+
830
+ Returns:
831
+ List of tuples: (tool_name, match_score, category)
832
+ Score is 0-100, higher is better match
833
+ """
834
+ if not query:
835
+ # Return all tools sorted alphabetically
836
+ tools = self.get_available_tools() if not include_unavailable else list(self._specs.keys())
837
+ return [(name, 100, self._specs[name].category) for name in sorted(tools)[:limit]]
838
+
839
+ # Build search data: name -> searchable text
840
+ search_data = {}
841
+ for name, spec in self._specs.items():
842
+ if not include_unavailable and not self.is_available(name):
843
+ continue
844
+ # Combine name and description for searching
845
+ search_text = f"{name} {spec.description}"
846
+ search_data[name] = search_text
847
+
848
+ if not search_data:
849
+ return []
850
+
851
+ results: List[Tuple[str, int, ToolCategory]] = []
852
+
853
+ if RAPIDFUZZ_AVAILABLE and process is not None:
854
+ # Use rapidfuzz for proper fuzzy matching
855
+ matches = process.extract(
856
+ query,
857
+ search_data,
858
+ scorer=fuzz.WRatio,
859
+ limit=limit
860
+ )
861
+
862
+ for match in matches:
863
+ tool_name = match[2] # Key from dict
864
+ score = int(match[1]) # Score 0-100
865
+ if score >= 30: # Minimum relevance threshold
866
+ category = self._specs[tool_name].category
867
+ results.append((tool_name, score, category))
868
+ else:
869
+ # Fallback to simple substring matching
870
+ query_lower = query.lower()
871
+ for name, search_text in search_data.items():
872
+ text_lower = search_text.lower()
873
+ if query_lower in text_lower:
874
+ # Score based on position and exact match
875
+ if name.lower() == query_lower:
876
+ score = 100
877
+ elif name.lower().startswith(query_lower):
878
+ score = 90
879
+ elif query_lower in name.lower():
880
+ score = 80
881
+ else:
882
+ score = 60 # Match in description only
883
+
884
+ category = self._specs[name].category
885
+ results.append((name, score, category))
886
+
887
+ # Sort by score descending, then name ascending
888
+ results.sort(key=lambda x: (-x[1], x[0]))
889
+ results = results[:limit]
890
+
891
+ return results
892
+
660
893
  def get_legacy_flags(self) -> Dict[str, bool]:
661
894
  """
662
895
  Get legacy availability flags for backwards compatibility.
@@ -671,26 +904,33 @@ class ToolLoader:
671
904
  return flags
672
905
 
673
906
 
674
- # Global instance
907
+ # Global instance with thread-safe initialization
675
908
  _tool_loader: Optional[ToolLoader] = None
909
+ _tool_loader_lock = threading.Lock()
676
910
 
677
911
 
678
912
  def get_tool_loader() -> ToolLoader:
679
913
  """
680
- Get the global tool loader instance.
914
+ Get the global tool loader instance (thread-safe).
915
+
916
+ Uses double-checked locking pattern to ensure thread safety
917
+ while minimizing lock contention after initialization.
681
918
 
682
919
  Returns:
683
920
  Global ToolLoader instance
684
921
  """
685
922
  global _tool_loader
686
923
  if _tool_loader is None:
687
- _tool_loader = ToolLoader()
924
+ with _tool_loader_lock:
925
+ # Double-check after acquiring lock
926
+ if _tool_loader is None:
927
+ _tool_loader = ToolLoader()
688
928
  return _tool_loader
689
929
 
690
930
 
691
931
  def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLoader:
692
932
  """
693
- Initialize the global tool loader.
933
+ Initialize the global tool loader (thread-safe).
694
934
 
695
935
  Args:
696
936
  tool_specs: Optional custom tool specifications
@@ -699,12 +939,13 @@ def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLo
699
939
  Initialized ToolLoader
700
940
  """
701
941
  global _tool_loader
702
- _tool_loader = ToolLoader(tool_specs)
942
+ with _tool_loader_lock:
943
+ _tool_loader = ToolLoader(tool_specs)
703
944
  return _tool_loader
704
945
 
705
946
 
706
947
  def reset_tool_loader() -> None:
707
- """Reset the global tool loader."""
948
+ """Reset the global tool loader (thread-safe)."""
708
949
  global _tool_loader
709
- _tool_loader = None
710
-
950
+ with _tool_loader_lock:
951
+ _tool_loader = None