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.
- package/bin/pomera-create-shortcut.js +51 -0
- package/bin/pomera.js +68 -0
- 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 +497 -0
- package/create_shortcut.py +12 -4
- package/mcp.json +1 -1
- package/package.json +4 -2
- package/pomera.py +408 -72
- package/tools/base64_tools.py +4 -4
- package/tools/curl_history.py +45 -5
- package/tools/curl_tool.py +42 -12
- package/tools/diff_viewer.py +2 -9
- package/tools/notes_widget.py +8 -1
- package/tools/tool_loader.py +265 -24
package/tools/base64_tools.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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."""
|
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/notes_widget.py
CHANGED
|
@@ -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."""
|
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",
|
|
@@ -294,20 +301,13 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
294
301
|
),
|
|
295
302
|
|
|
296
303
|
# Analysis Tools
|
|
297
|
-
"Word Frequency Counter"
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
710
|
-
|
|
950
|
+
with _tool_loader_lock:
|
|
951
|
+
_tool_loader = None
|