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.
@@ -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=1.0, anchor="ne")
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
- "Word Frequency Counter", "HTML Tool",
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
- results = process.extract(query.lower(), search_data, scorer=fuzz.WRatio, limit=15)
271
- return [match[2] for match in results if match[1] >= 40]
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):
@@ -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
- # First check if we're running from the package itself
22
+ # Get the directory where this script is located
23
23
  script_dir = Path(__file__).parent.resolve()
24
- if (script_dir / "pomera.py").exists():
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 npm_path.exists():
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.7",
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.7",
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
- if "pattern_library" not in self.app.settings:
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
- self.app.logger.info(f"Loaded comprehensive pattern library with {len(self.app.settings['pattern_library'])} patterns")
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"] = [
@@ -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."""
@@ -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."""
@@ -301,20 +301,13 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
301
301
  ),
302
302
 
303
303
  # Analysis Tools
304
- "Word Frequency Counter": ToolSpec(
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="Calculate text statistics (chars, words, lines)",
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(