pomera-ai-commander 1.2.7 → 1.2.9
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/database_schema.py +24 -1
- package/core/database_schema_manager.py +4 -2
- package/core/database_settings_manager.py +25 -2
- package/core/dialog_manager.py +4 -4
- package/core/efficient_line_numbers.py +5 -4
- package/core/load_presets_dialog.py +460 -0
- package/core/mcp/tool_registry.py +327 -0
- package/core/settings_defaults_registry.py +159 -15
- package/core/tool_search_widget.py +85 -5
- package/create_shortcut.py +12 -4
- package/mcp.json +1 -1
- package/package.json +4 -2
- package/pomera.py +760 -25
- package/tools/base64_tools.py +4 -4
- package/tools/case_tool.py +4 -4
- package/tools/curl_settings.py +12 -1
- package/tools/curl_tool.py +176 -11
- package/tools/notes_widget.py +8 -1
- package/tools/tool_loader.py +20 -9
- package/tools/url_content_reader.py +402 -0
- package/tools/web_search.py +522 -0
|
@@ -73,6 +73,7 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
73
73
|
self._selected_index: int = 0
|
|
74
74
|
self._popup: Optional[tk.Toplevel] = None
|
|
75
75
|
self._popup_listbox: Optional[tk.Listbox] = None
|
|
76
|
+
self._closing: bool = False # Flag to prevent re-opening during close
|
|
76
77
|
|
|
77
78
|
self._create_widgets()
|
|
78
79
|
|
|
@@ -116,6 +117,9 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
116
117
|
|
|
117
118
|
def _on_search_change(self, *args) -> None:
|
|
118
119
|
"""Handle text changes in search entry (via StringVar trace)."""
|
|
120
|
+
# Don't show popup during close operation
|
|
121
|
+
if self._closing:
|
|
122
|
+
return
|
|
119
123
|
# Show popup if not visible
|
|
120
124
|
if not (self._popup and self._popup.winfo_exists()):
|
|
121
125
|
self._show_popup()
|
|
@@ -164,6 +168,16 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
164
168
|
popup_frame = ttk.Frame(self._popup, relief="solid", borderwidth=1)
|
|
165
169
|
popup_frame.pack(fill=tk.BOTH, expand=True)
|
|
166
170
|
|
|
171
|
+
# Header frame with title only
|
|
172
|
+
header_frame = ttk.Frame(popup_frame)
|
|
173
|
+
header_frame.pack(fill=tk.X, padx=2, pady=(2, 0))
|
|
174
|
+
|
|
175
|
+
# Header label
|
|
176
|
+
ttk.Label(header_frame, text="Select Tool", font=("TkDefaultFont", 9, "bold")).pack(side=tk.LEFT, padx=5)
|
|
177
|
+
|
|
178
|
+
# Separator under header
|
|
179
|
+
ttk.Separator(popup_frame, orient="horizontal").pack(fill=tk.X, padx=2, pady=2)
|
|
180
|
+
|
|
167
181
|
# Results listbox
|
|
168
182
|
self._popup_listbox = tk.Listbox(
|
|
169
183
|
popup_frame,
|
|
@@ -175,9 +189,17 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
175
189
|
|
|
176
190
|
# Scrollbar
|
|
177
191
|
scrollbar = ttk.Scrollbar(popup_frame, command=self._popup_listbox.yview)
|
|
178
|
-
scrollbar.place(relx=1.0, rely=0, relheight=
|
|
192
|
+
scrollbar.place(relx=1.0, rely=0, relheight=0.85, anchor="ne")
|
|
179
193
|
self._popup_listbox.configure(yscrollcommand=scrollbar.set)
|
|
180
194
|
|
|
195
|
+
# Footer frame with close button at bottom right
|
|
196
|
+
footer_frame = ttk.Frame(popup_frame)
|
|
197
|
+
footer_frame.pack(fill=tk.X, padx=2, pady=(0, 2))
|
|
198
|
+
|
|
199
|
+
# Close button (X) - positioned at bottom right
|
|
200
|
+
close_btn = ttk.Button(footer_frame, text="Close", width=8, command=self._close_and_select_default)
|
|
201
|
+
close_btn.pack(side=tk.RIGHT, padx=2)
|
|
202
|
+
|
|
181
203
|
# Bind events
|
|
182
204
|
self._popup_listbox.bind("<Double-Button-1>", self._on_listbox_select)
|
|
183
205
|
self._popup_listbox.bind("<Return>", self._on_listbox_select)
|
|
@@ -193,7 +215,7 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
193
215
|
SUB_TOOLS = {
|
|
194
216
|
"Slug Generator", "Hash Generator", "ASCII Art Generator",
|
|
195
217
|
"URL Link Extractor", "Regex Extractor", "Email Extraction",
|
|
196
|
-
"
|
|
218
|
+
"HTML Tool",
|
|
197
219
|
}
|
|
198
220
|
|
|
199
221
|
def _update_popup_list(self) -> None:
|
|
@@ -255,6 +277,16 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
255
277
|
if not query:
|
|
256
278
|
return tools
|
|
257
279
|
|
|
280
|
+
query_lower = query.lower()
|
|
281
|
+
|
|
282
|
+
# For very short queries (1-2 chars), use prefix matching only to avoid noise
|
|
283
|
+
if len(query) <= 2:
|
|
284
|
+
matches = [t for t in tools if t.lower().startswith(query_lower)]
|
|
285
|
+
if matches:
|
|
286
|
+
return matches
|
|
287
|
+
# Fallback to contains if no prefix matches
|
|
288
|
+
return [t for t in tools if query_lower in t.lower()]
|
|
289
|
+
|
|
258
290
|
if RAPIDFUZZ_AVAILABLE:
|
|
259
291
|
search_data = {}
|
|
260
292
|
for tool in tools:
|
|
@@ -267,11 +299,23 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
267
299
|
search_data[tool] = search_text.lower()
|
|
268
300
|
|
|
269
301
|
# Convert query to lowercase for case-insensitive search
|
|
270
|
-
|
|
271
|
-
|
|
302
|
+
# Threshold set to 50 to allow substring matches
|
|
303
|
+
results = process.extract(query_lower, search_data, scorer=fuzz.WRatio, limit=15)
|
|
304
|
+
fuzzy_matches = [match[2] for match in results if match[1] >= 50]
|
|
305
|
+
|
|
306
|
+
# Also include any tools that contain the query as substring (ensures "URL" finds "URL Parser")
|
|
307
|
+
substring_matches = [t for t in tools if query_lower in t.lower()]
|
|
308
|
+
for tool in substring_matches:
|
|
309
|
+
if tool not in fuzzy_matches:
|
|
310
|
+
fuzzy_matches.append(tool)
|
|
311
|
+
|
|
312
|
+
# Prioritize exact prefix matches at the top
|
|
313
|
+
prefix_matches = [t for t in fuzzy_matches if t.lower().startswith(query_lower)]
|
|
314
|
+
other_matches = [t for t in fuzzy_matches if not t.lower().startswith(query_lower)]
|
|
315
|
+
|
|
316
|
+
return prefix_matches + other_matches
|
|
272
317
|
else:
|
|
273
318
|
# Fallback substring matching
|
|
274
|
-
query_lower = query.lower()
|
|
275
319
|
matches = [(t, t.lower().find(query_lower)) for t in tools if query_lower in t.lower()]
|
|
276
320
|
matches.sort(key=lambda x: x[1])
|
|
277
321
|
return [m[0] for m in matches]
|
|
@@ -360,6 +404,39 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
360
404
|
|
|
361
405
|
return "break"
|
|
362
406
|
|
|
407
|
+
def _close_and_select_default(self) -> None:
|
|
408
|
+
"""Close popup and select default tool if no tool selected."""
|
|
409
|
+
# Set closing flag to prevent focus events from re-opening
|
|
410
|
+
self._closing = True
|
|
411
|
+
|
|
412
|
+
# Destroy popup FIRST to prevent any refresh loops
|
|
413
|
+
if self._popup and self._popup.winfo_exists():
|
|
414
|
+
self._popup.destroy()
|
|
415
|
+
self._popup = None
|
|
416
|
+
self._popup_listbox = None
|
|
417
|
+
|
|
418
|
+
# If no current tool, default to AI Tools
|
|
419
|
+
if not self._current_tool or self._current_tool == "":
|
|
420
|
+
self._current_tool = "AI Tools"
|
|
421
|
+
if self._on_tool_selected:
|
|
422
|
+
try:
|
|
423
|
+
self._on_tool_selected(self._current_tool)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"Error in on_tool_selected callback: {e}")
|
|
426
|
+
|
|
427
|
+
# Set the search var to current tool (popup already destroyed, won't trigger refresh)
|
|
428
|
+
self._search_var.set(self._current_tool)
|
|
429
|
+
|
|
430
|
+
# Remove focus from entry
|
|
431
|
+
self.winfo_toplevel().focus_set()
|
|
432
|
+
|
|
433
|
+
# Reset closing flag after a delay
|
|
434
|
+
self.after(200, self._reset_closing_flag)
|
|
435
|
+
|
|
436
|
+
def _reset_closing_flag(self) -> None:
|
|
437
|
+
"""Reset the closing flag."""
|
|
438
|
+
self._closing = False
|
|
439
|
+
|
|
363
440
|
def _on_popup_focus_out(self, event=None) -> None:
|
|
364
441
|
"""Handle focus leaving popup."""
|
|
365
442
|
# Delay to check if focus went to entry
|
|
@@ -372,6 +449,9 @@ class ToolSearchPalette(ttk.Frame):
|
|
|
372
449
|
|
|
373
450
|
def _check_focus(self) -> None:
|
|
374
451
|
"""Check if focus is on entry or popup, hide if not."""
|
|
452
|
+
# Don't interfere during close operation
|
|
453
|
+
if self._closing:
|
|
454
|
+
return
|
|
375
455
|
try:
|
|
376
456
|
focused = self.focus_get()
|
|
377
457
|
if focused not in (self.tool_entry, self._popup_listbox):
|
package/create_shortcut.py
CHANGED
|
@@ -19,12 +19,15 @@ from pathlib import Path
|
|
|
19
19
|
|
|
20
20
|
def get_package_dir() -> Path:
|
|
21
21
|
"""Get the pomera-ai-commander package directory."""
|
|
22
|
-
#
|
|
22
|
+
# Get the directory where this script is located
|
|
23
23
|
script_dir = Path(__file__).parent.resolve()
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
# Check if we're already running from an npm installation
|
|
26
|
+
# (script_dir will be inside node_modules/pomera-ai-commander)
|
|
27
|
+
if "node_modules" in str(script_dir) and (script_dir / "pomera.py").exists():
|
|
25
28
|
return script_dir
|
|
26
29
|
|
|
27
|
-
# Check npm global installation
|
|
30
|
+
# Check npm global installation paths
|
|
28
31
|
npm_global = os.environ.get("APPDATA", "") or os.path.expanduser("~")
|
|
29
32
|
if platform.system() == "Windows":
|
|
30
33
|
npm_path = Path(npm_global) / "npm" / "node_modules" / "pomera-ai-commander"
|
|
@@ -35,9 +38,14 @@ def get_package_dir() -> Path:
|
|
|
35
38
|
if not npm_path.exists():
|
|
36
39
|
npm_path = Path.home() / ".npm-packages" / "lib" / "node_modules" / "pomera-ai-commander"
|
|
37
40
|
|
|
38
|
-
if
|
|
41
|
+
# Prefer npm installation if it exists
|
|
42
|
+
if npm_path.exists() and (npm_path / "pomera.py").exists():
|
|
39
43
|
return npm_path
|
|
40
44
|
|
|
45
|
+
# Fallback to script's directory (for pip install or direct run)
|
|
46
|
+
if (script_dir / "pomera.py").exists():
|
|
47
|
+
return script_dir
|
|
48
|
+
|
|
41
49
|
return script_dir
|
|
42
50
|
|
|
43
51
|
|
package/mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pomera-ai-commander",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Text processing toolkit with 22 MCP tools including case transformation, encoding, hashing, text analysis, and notes management for AI assistants.",
|
|
5
5
|
"homepage": "https://github.com/matbanik/Pomera-AI-Commander",
|
|
6
6
|
"repository": "https://github.com/matbanik/Pomera-AI-Commander",
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pomera-ai-commander",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Text processing toolkit with 22 MCP tools for AI assistants - case transformation, encoding, hashing, text analysis, and notes management",
|
|
5
5
|
"main": "pomera_mcp_server.py",
|
|
6
6
|
"bin": {
|
|
7
7
|
"pomera-ai-commander": "./bin/pomera-ai-commander.js",
|
|
8
|
-
"pomera-mcp": "./bin/pomera-ai-commander.js"
|
|
8
|
+
"pomera-mcp": "./bin/pomera-ai-commander.js",
|
|
9
|
+
"pomera": "./bin/pomera.js",
|
|
10
|
+
"pomera-create-shortcut": "./bin/pomera-create-shortcut.js"
|
|
9
11
|
},
|
|
10
12
|
"scripts": {
|
|
11
13
|
"start": "python pomera_mcp_server.py",
|