pomera-ai-commander 1.2.7 → 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/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 +5 -3
- package/tools/base64_tools.py +4 -4
- package/tools/notes_widget.py +8 -1
- package/tools/tool_loader.py +2 -9
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pomera AI Commander - Desktop Shortcut Creator
|
|
5
|
+
*
|
|
6
|
+
* This script creates a desktop shortcut for the Pomera GUI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Get the path to create_shortcut.py
|
|
13
|
+
const shortcutScript = path.join(__dirname, '..', 'create_shortcut.py');
|
|
14
|
+
|
|
15
|
+
// Find Python executable
|
|
16
|
+
function findPython() {
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
21
|
+
return 'python3';
|
|
22
|
+
} catch (e) {
|
|
23
|
+
try {
|
|
24
|
+
execSync('python --version', { stdio: 'ignore' });
|
|
25
|
+
return 'python';
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('Error: Python is not installed or not in PATH');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pythonCmd = findPython();
|
|
34
|
+
|
|
35
|
+
// Get command line arguments
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
|
|
38
|
+
// Spawn the Python script
|
|
39
|
+
const proc = spawn(pythonCmd, [shortcutScript, ...args], {
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
cwd: path.join(__dirname, '..')
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
proc.on('close', (code) => {
|
|
45
|
+
process.exit(code || 0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
proc.on('error', (err) => {
|
|
49
|
+
console.error('Failed to create shortcut:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
package/bin/pomera.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pomera AI Commander - GUI launcher
|
|
5
|
+
*
|
|
6
|
+
* This script launches the Pomera GUI application.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Get the path to pomera.py
|
|
13
|
+
const pomeraPath = path.join(__dirname, '..', 'pomera.py');
|
|
14
|
+
|
|
15
|
+
// Find Python executable
|
|
16
|
+
function findPython() {
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
// Try pythonw first (Windows - no console)
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
try {
|
|
22
|
+
execSync('pythonw --version', { stdio: 'ignore' });
|
|
23
|
+
return 'pythonw';
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// Fall through
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Try python3 (Linux/macOS)
|
|
30
|
+
try {
|
|
31
|
+
execSync('python3 --version', { stdio: 'ignore' });
|
|
32
|
+
return 'python3';
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Fall back to python
|
|
35
|
+
try {
|
|
36
|
+
execSync('python --version', { stdio: 'ignore' });
|
|
37
|
+
return 'python';
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.error('Error: Python is not installed or not in PATH');
|
|
40
|
+
console.error('Please install Python 3.8 or higher');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pythonCmd = findPython();
|
|
47
|
+
|
|
48
|
+
// Spawn the Python GUI
|
|
49
|
+
const app = spawn(pythonCmd, [pomeraPath], {
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
cwd: path.join(__dirname, '..'),
|
|
52
|
+
detached: process.platform !== 'win32' // Detach on non-Windows
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Handle process exit
|
|
56
|
+
app.on('close', (code) => {
|
|
57
|
+
process.exit(code || 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Handle errors
|
|
61
|
+
app.on('error', (err) => {
|
|
62
|
+
console.error('Failed to start Pomera:', err.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Forward signals
|
|
67
|
+
process.on('SIGINT', () => app.kill('SIGINT'));
|
|
68
|
+
process.on('SIGTERM', () => app.kill('SIGTERM'));
|
|
@@ -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.8",
|
|
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.8",
|
|
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",
|
package/pomera.py
CHANGED
|
@@ -626,14 +626,16 @@ class PromeraAISettingsManager:
|
|
|
626
626
|
|
|
627
627
|
def get_pattern_library(self) -> List[Dict[str, str]]:
|
|
628
628
|
"""Get the regex pattern library."""
|
|
629
|
-
# Initialize pattern library if it doesn't exist
|
|
630
|
-
|
|
629
|
+
# Initialize pattern library if it doesn't exist OR is empty
|
|
630
|
+
existing = self.app.settings.get("pattern_library", None)
|
|
631
|
+
if not existing or len(existing) == 0:
|
|
631
632
|
# Try to import and use the comprehensive pattern library
|
|
632
633
|
try:
|
|
633
634
|
from core.regex_pattern_library import RegexPatternLibrary
|
|
634
635
|
library = RegexPatternLibrary()
|
|
635
636
|
self.app.settings["pattern_library"] = library._convert_to_settings_format()
|
|
636
|
-
|
|
637
|
+
pattern_count = len(self.app.settings.get("pattern_library", []))
|
|
638
|
+
self.app.logger.info(f"Loaded comprehensive pattern library with {pattern_count} patterns")
|
|
637
639
|
except ImportError:
|
|
638
640
|
# Fallback to basic patterns if comprehensive library is not available
|
|
639
641
|
self.app.settings["pattern_library"] = [
|
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/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
|
@@ -301,20 +301,13 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
|
|
|
301
301
|
),
|
|
302
302
|
|
|
303
303
|
# Analysis Tools
|
|
304
|
-
"Word Frequency Counter"
|
|
305
|
-
name="Word Frequency Counter",
|
|
306
|
-
module_path="tools.word_frequency_counter",
|
|
307
|
-
class_name="WordFrequencyCounter",
|
|
308
|
-
category=ToolCategory.ANALYSIS,
|
|
309
|
-
description="Count word frequencies in text",
|
|
310
|
-
available_flag="WORD_FREQUENCY_COUNTER_MODULE_AVAILABLE"
|
|
311
|
-
),
|
|
304
|
+
# NOTE: Word Frequency Counter merged into Text Statistics (has "Word Frequency Counter" button)
|
|
312
305
|
"Text Statistics": ToolSpec(
|
|
313
306
|
name="Text Statistics",
|
|
314
307
|
module_path="tools.text_statistics_tool",
|
|
315
308
|
class_name="TextStatistics",
|
|
316
309
|
category=ToolCategory.ANALYSIS,
|
|
317
|
-
description="
|
|
310
|
+
description="Text stats, character/word/line counts, word frequency",
|
|
318
311
|
available_flag="TEXT_STATISTICS_MODULE_AVAILABLE"
|
|
319
312
|
),
|
|
320
313
|
"Cron Tool": ToolSpec(
|