pomera-ai-commander 1.2.2 → 1.2.4

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/README.md CHANGED
@@ -1,4 +1,9 @@
1
1
  # Pomera AI Commander (PAC)
2
+
3
+ <p align="center">
4
+ <img src="resources/icon.png" alt="Pomera - the fluffy Pomeranian mascot" width="128" height="128">
5
+ </p>
6
+
2
7
  [![Download Latest Release](https://img.shields.io/badge/Download-Latest%20Release-blue?style=for-the-badge&logo=github)](https://github.com/matbanik/Pomera-AI-Commander/releases)
3
8
 
4
9
  A desktop text "workbench" + MCP server: clean, transform, extract, and analyze text fast—manually in a GUI or programmatically from AI assistants (Cursor / Claude Desktop / MCP clients).
@@ -1868,13 +1868,41 @@ class ToolRegistry:
1868
1868
  conn.row_factory = sqlite3.Row
1869
1869
  return conn
1870
1870
 
1871
+ def _sanitize_text(self, text: str) -> str:
1872
+ """
1873
+ Sanitize text by removing invalid UTF-8 surrogate characters.
1874
+
1875
+ Lone surrogates (U+D800 to U+DFFF) are invalid in UTF-8 and cause
1876
+ encoding errors. This function removes them while preserving valid content.
1877
+
1878
+ Args:
1879
+ text: Input text that may contain invalid surrogates
1880
+
1881
+ Returns:
1882
+ Sanitized text safe for UTF-8 encoding and database storage
1883
+ """
1884
+ if not text:
1885
+ return text
1886
+
1887
+ # Encode to UTF-8 with 'surrogatepass' to handle surrogates,
1888
+ # then decode with 'replace' to replace invalid sequences
1889
+ try:
1890
+ # This two-step process handles lone surrogates:
1891
+ # 1. surrogatepass allows encoding surrogates (normally forbidden in UTF-8)
1892
+ # 2. errors='replace' replaces invalid sequences with replacement char
1893
+ sanitized = text.encode('utf-8', errors='surrogatepass').decode('utf-8', errors='replace')
1894
+ return sanitized
1895
+ except Exception:
1896
+ # Fallback: manually filter out surrogate characters
1897
+ return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
1898
+
1871
1899
  def _handle_notes_save(self, args: Dict[str, Any]) -> str:
1872
1900
  """Handle saving a new note."""
1873
1901
  from datetime import datetime
1874
1902
 
1875
- title = args.get("title", "")
1876
- input_content = args.get("input_content", "")
1877
- output_content = args.get("output_content", "")
1903
+ title = self._sanitize_text(args.get("title", ""))
1904
+ input_content = self._sanitize_text(args.get("input_content", ""))
1905
+ output_content = self._sanitize_text(args.get("output_content", ""))
1878
1906
 
1879
1907
  if not title:
1880
1908
  return "Error: Title is required"
@@ -2042,15 +2070,15 @@ class ToolRegistry:
2042
2070
 
2043
2071
  if "title" in args:
2044
2072
  updates.append("Title = ?")
2045
- values.append(args["title"])
2073
+ values.append(self._sanitize_text(args["title"]))
2046
2074
 
2047
2075
  if "input_content" in args:
2048
2076
  updates.append("Input = ?")
2049
- values.append(args["input_content"])
2077
+ values.append(self._sanitize_text(args["input_content"]))
2050
2078
 
2051
2079
  if "output_content" in args:
2052
2080
  updates.append("Output = ?")
2053
- values.append(args["output_content"])
2081
+ values.append(self._sanitize_text(args["output_content"]))
2054
2082
 
2055
2083
  if not updates:
2056
2084
  conn.close()
@@ -2639,12 +2667,17 @@ class ToolRegistry:
2639
2667
  """Save operation to notes and return note_id."""
2640
2668
  try:
2641
2669
  from datetime import datetime
2670
+ # Sanitize text to prevent UTF-8 surrogate errors
2671
+ sanitized_title = registry._sanitize_text(title)
2672
+ sanitized_input = registry._sanitize_text(input_content)
2673
+ sanitized_output = registry._sanitize_text(output_content)
2674
+
2642
2675
  conn = registry._get_notes_connection()
2643
2676
  now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
2644
2677
  cursor = conn.execute('''
2645
2678
  INSERT INTO notes (Created, Modified, Title, Input, Output)
2646
2679
  VALUES (?, ?, ?, ?, ?)
2647
- ''', (now, now, title, input_content, output_content))
2680
+ ''', (now, now, sanitized_title, sanitized_input, sanitized_output))
2648
2681
  note_id = cursor.lastrowid
2649
2682
  conn.commit()
2650
2683
  conn.close()
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Create Desktop Shortcut for Pomera AI Commander
4
+
5
+ Cross-platform script to create desktop shortcuts/launchers:
6
+ - Windows: Creates .lnk shortcut
7
+ - macOS: Creates .command script in ~/Desktop
8
+ - Linux: Creates .desktop launcher
9
+
10
+ Can be run standalone or as npm postinstall hook.
11
+ """
12
+
13
+ import os
14
+ import sys
15
+ import stat
16
+ import platform
17
+ from pathlib import Path
18
+
19
+
20
+ def get_package_dir() -> Path:
21
+ """Get the pomera-ai-commander package directory."""
22
+ # First check if we're running from the package itself
23
+ script_dir = Path(__file__).parent.resolve()
24
+ if (script_dir / "pomera.py").exists():
25
+ return script_dir
26
+
27
+ # Check npm global installation
28
+ npm_global = os.environ.get("APPDATA", "") or os.path.expanduser("~")
29
+ if platform.system() == "Windows":
30
+ npm_path = Path(npm_global) / "npm" / "node_modules" / "pomera-ai-commander"
31
+ else:
32
+ npm_path = Path(npm_global) / ".npm-global" / "lib" / "node_modules" / "pomera-ai-commander"
33
+ if not npm_path.exists():
34
+ npm_path = Path("/usr/local/lib/node_modules/pomera-ai-commander")
35
+ if not npm_path.exists():
36
+ npm_path = Path.home() / ".npm-packages" / "lib" / "node_modules" / "pomera-ai-commander"
37
+
38
+ if npm_path.exists():
39
+ return npm_path
40
+
41
+ return script_dir
42
+
43
+
44
+ def get_desktop_path() -> Path:
45
+ """Get the user's desktop directory."""
46
+ if platform.system() == "Windows":
47
+ return Path(os.environ.get("USERPROFILE", "")) / "Desktop"
48
+ elif platform.system() == "Darwin": # macOS
49
+ return Path.home() / "Desktop"
50
+ else: # Linux
51
+ # Check XDG user dirs
52
+ xdg_desktop = os.environ.get("XDG_DESKTOP_DIR")
53
+ if xdg_desktop:
54
+ return Path(xdg_desktop)
55
+ return Path.home() / "Desktop"
56
+
57
+
58
+ def create_windows_shortcut(package_dir: Path, desktop: Path) -> bool:
59
+ """Create Windows .lnk shortcut."""
60
+ try:
61
+ import subprocess
62
+
63
+ shortcut_path = desktop / "Pomera AI Commander.lnk"
64
+ pomera_py = package_dir / "pomera.py"
65
+ icon_path = package_dir / "resources" / "icon.ico"
66
+
67
+ # Use PowerShell to create shortcut
68
+ ps_script = f'''
69
+ $WshShell = New-Object -ComObject WScript.Shell
70
+ $Shortcut = $WshShell.CreateShortcut("{shortcut_path}")
71
+ $Shortcut.TargetPath = "pythonw.exe"
72
+ $Shortcut.Arguments = '"{pomera_py}"'
73
+ $Shortcut.WorkingDirectory = "{package_dir}"
74
+ $Shortcut.Description = "Pomera AI Commander - Text Processing Toolkit"
75
+ '''
76
+ if icon_path.exists():
77
+ ps_script += f'$Shortcut.IconLocation = "{icon_path},0"\n'
78
+
79
+ ps_script += '$Shortcut.Save()'
80
+
81
+ result = subprocess.run(
82
+ ["powershell", "-Command", ps_script],
83
+ capture_output=True,
84
+ text=True
85
+ )
86
+
87
+ if result.returncode == 0:
88
+ print(f"✓ Created Windows shortcut: {shortcut_path}")
89
+ return True
90
+ else:
91
+ print(f"✗ Failed to create shortcut: {result.stderr}")
92
+ return False
93
+
94
+ except Exception as e:
95
+ print(f"✗ Error creating Windows shortcut: {e}")
96
+ return False
97
+
98
+
99
+ def create_macos_shortcut(package_dir: Path, desktop: Path) -> bool:
100
+ """Create macOS .command script."""
101
+ try:
102
+ shortcut_path = desktop / "Pomera AI Commander.command"
103
+ pomera_py = package_dir / "pomera.py"
104
+
105
+ script_content = f'''#!/bin/bash
106
+ # Pomera AI Commander Launcher
107
+ cd "{package_dir}"
108
+ python3 "{pomera_py}"
109
+ '''
110
+
111
+ shortcut_path.write_text(script_content)
112
+ # Make executable
113
+ shortcut_path.chmod(shortcut_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
114
+
115
+ print(f"✓ Created macOS launcher: {shortcut_path}")
116
+ return True
117
+
118
+ except Exception as e:
119
+ print(f"✗ Error creating macOS shortcut: {e}")
120
+ return False
121
+
122
+
123
+ def create_linux_shortcut(package_dir: Path, desktop: Path) -> bool:
124
+ """Create Linux .desktop launcher."""
125
+ try:
126
+ shortcut_path = desktop / "pomera-ai-commander.desktop"
127
+ pomera_py = package_dir / "pomera.py"
128
+ icon_path = package_dir / "resources" / "icon.png"
129
+
130
+ # Use icon if exists, otherwise use generic
131
+ icon = str(icon_path) if icon_path.exists() else "utilities-terminal"
132
+
133
+ desktop_entry = f'''[Desktop Entry]
134
+ Version=1.0
135
+ Type=Application
136
+ Name=Pomera AI Commander
137
+ Comment=Text Processing Toolkit with MCP tools for AI assistants
138
+ Exec=python3 "{pomera_py}"
139
+ Icon={icon}
140
+ Terminal=false
141
+ Categories=Development;Utility;TextTools;
142
+ StartupNotify=true
143
+ '''
144
+
145
+ shortcut_path.write_text(desktop_entry)
146
+ # Make executable
147
+ shortcut_path.chmod(shortcut_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
148
+
149
+ print(f"✓ Created Linux launcher: {shortcut_path}")
150
+
151
+ # Also try to add to applications menu
152
+ try:
153
+ apps_dir = Path.home() / ".local" / "share" / "applications"
154
+ apps_dir.mkdir(parents=True, exist_ok=True)
155
+ apps_shortcut = apps_dir / "pomera-ai-commander.desktop"
156
+ apps_shortcut.write_text(desktop_entry)
157
+ apps_shortcut.chmod(apps_shortcut.stat().st_mode | stat.S_IXUSR)
158
+ print(f"✓ Added to applications menu: {apps_shortcut}")
159
+ except Exception:
160
+ pass # Not critical
161
+
162
+ return True
163
+
164
+ except Exception as e:
165
+ print(f"✗ Error creating Linux shortcut: {e}")
166
+ return False
167
+
168
+
169
+ def create_shortcut():
170
+ """Create desktop shortcut for the current platform."""
171
+ print("\n🐕 Pomera AI Commander - Desktop Shortcut Creator")
172
+ print("=" * 50)
173
+
174
+ package_dir = get_package_dir()
175
+ desktop = get_desktop_path()
176
+
177
+ print(f"Package directory: {package_dir}")
178
+ print(f"Desktop directory: {desktop}")
179
+
180
+ if not (package_dir / "pomera.py").exists():
181
+ print(f"✗ Error: pomera.py not found in {package_dir}")
182
+ return False
183
+
184
+ if not desktop.exists():
185
+ print(f"✗ Error: Desktop directory not found: {desktop}")
186
+ return False
187
+
188
+ system = platform.system()
189
+ print(f"Platform: {system}")
190
+ print("-" * 50)
191
+
192
+ if system == "Windows":
193
+ return create_windows_shortcut(package_dir, desktop)
194
+ elif system == "Darwin":
195
+ return create_macos_shortcut(package_dir, desktop)
196
+ elif system == "Linux":
197
+ return create_linux_shortcut(package_dir, desktop)
198
+ else:
199
+ print(f"✗ Unsupported platform: {system}")
200
+ return False
201
+
202
+
203
+ def remove_shortcut():
204
+ """Remove desktop shortcut for the current platform."""
205
+ print("\n🐕 Removing Pomera AI Commander shortcut...")
206
+
207
+ desktop = get_desktop_path()
208
+ system = platform.system()
209
+
210
+ shortcuts = []
211
+ if system == "Windows":
212
+ shortcuts.append(desktop / "Pomera AI Commander.lnk")
213
+ elif system == "Darwin":
214
+ shortcuts.append(desktop / "Pomera AI Commander.command")
215
+ elif system == "Linux":
216
+ shortcuts.append(desktop / "pomera-ai-commander.desktop")
217
+ shortcuts.append(Path.home() / ".local" / "share" / "applications" / "pomera-ai-commander.desktop")
218
+
219
+ for shortcut in shortcuts:
220
+ if shortcut.exists():
221
+ try:
222
+ shortcut.unlink()
223
+ print(f"✓ Removed: {shortcut}")
224
+ except Exception as e:
225
+ print(f"✗ Failed to remove {shortcut}: {e}")
226
+
227
+
228
+ if __name__ == "__main__":
229
+ if len(sys.argv) > 1 and sys.argv[1] == "--remove":
230
+ remove_shortcut()
231
+ else:
232
+ success = create_shortcut()
233
+ if success:
234
+ print("\n✅ Desktop shortcut created successfully!")
235
+ else:
236
+ print("\n⚠️ Failed to create desktop shortcut")
237
+ sys.exit(1)
package/mcp.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pomera-ai-commander",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pomera-ai-commander",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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": {
@@ -50,8 +50,10 @@
50
50
  "core/",
51
51
  "tools/",
52
52
  "scripts/",
53
+ "resources/",
53
54
  "pomera_mcp_server.py",
54
55
  "pomera.py",
56
+ "create_shortcut.py",
55
57
  "migrate_data.py",
56
58
  "mcp.json",
57
59
  "README.md",
package/pomera.py CHANGED
@@ -779,6 +779,9 @@ class PromeraAIApp(tk.Tk):
779
779
 
780
780
  self.title("Pomera AI Commander")
781
781
  self.geometry(AppConfig.DEFAULT_WINDOW_SIZE)
782
+
783
+ # Set window icon (Pomera dog mascot)
784
+ self._set_window_icon()
782
785
 
783
786
  self._after_id = None
784
787
  self._regex_cache = {}
@@ -1201,7 +1204,7 @@ class PromeraAIApp(tk.Tk):
1201
1204
  pyproject_path = Path(__file__).parent / "pyproject.toml"
1202
1205
  if pyproject_path.exists():
1203
1206
  content = pyproject_path.read_text()
1204
- match = re.search(r'version = "([^"]+)"', content)
1207
+ match = re.search(r'version = "1.2.4"]+)"', content)
1205
1208
  if match:
1206
1209
  current_version = match.group(1)
1207
1210
  except Exception:
@@ -1212,7 +1215,7 @@ class PromeraAIApp(tk.Tk):
1212
1215
  import importlib.metadata
1213
1216
  current_version = importlib.metadata.version("pomera-ai-commander")
1214
1217
  except Exception:
1215
- current_version = "Unknown"
1218
+ current_version = "1.2.4"
1216
1219
 
1217
1220
  # Detect OS for download link
1218
1221
  system = platform.system()
@@ -1350,11 +1353,8 @@ class PromeraAIApp(tk.Tk):
1350
1353
 
1351
1354
  def _show_about_dialog(self):
1352
1355
  """Show About dialog."""
1353
- try:
1354
- import importlib.metadata
1355
- version = importlib.metadata.version("pomera-ai-commander")
1356
- except Exception:
1357
- version = "1.2.0"
1356
+ # Version managed by bump_version.py script
1357
+ version = "1.2.4"
1358
1358
 
1359
1359
  messagebox.showinfo(
1360
1360
  "About Pomera AI Commander",
@@ -1362,6 +1362,42 @@ class PromeraAIApp(tk.Tk):
1362
1362
  "Text processing toolkit with MCP tools for AI assistants.\n\n"
1363
1363
  "https://github.com/matbanik/Pomera-AI-Commander"
1364
1364
  )
1365
+
1366
+ def _set_window_icon(self):
1367
+ """Set the window icon (Pomera dog mascot)."""
1368
+ try:
1369
+ # Find icon file - check multiple locations
1370
+ script_dir = Path(__file__).parent.resolve()
1371
+ icon_locations = [
1372
+ script_dir / "resources" / "icon.ico",
1373
+ script_dir / "icon.ico",
1374
+ script_dir / "resources" / "icon.png",
1375
+ script_dir / "icon.png",
1376
+ ]
1377
+
1378
+ icon_path = None
1379
+ for loc in icon_locations:
1380
+ if loc.exists():
1381
+ icon_path = loc
1382
+ break
1383
+
1384
+ if icon_path is None:
1385
+ return # No icon found, use default
1386
+
1387
+ if platform.system() == "Windows" and icon_path.suffix == ".ico":
1388
+ # Windows: use iconbitmap for .ico files
1389
+ self.iconbitmap(str(icon_path))
1390
+ else:
1391
+ # Other platforms or PNG: use PhotoImage
1392
+ from PIL import Image, ImageTk
1393
+ img = Image.open(icon_path)
1394
+ # Resize to appropriate icon size - use NEAREST for sharp pixel art
1395
+ img = img.resize((32, 32), Image.Resampling.NEAREST)
1396
+ self._icon_photo = ImageTk.PhotoImage(img)
1397
+ self.iconphoto(True, self._icon_photo)
1398
+
1399
+ except Exception as e:
1400
+ self.logger.debug(f"Could not set window icon: {e}")
1365
1401
 
1366
1402
  def _auto_save_settings(self):
1367
1403
  """Auto-save settings periodically (called by Task Scheduler)."""
@@ -70,7 +70,7 @@ def main():
70
70
  parser.add_argument(
71
71
  "--version",
72
72
  action="version",
73
- version="pomera-mcp-server 1.2.2"
73
+ version="pomera-mcp-server 1.2.4"
74
74
  )
75
75
  parser.add_argument(
76
76
  "--list-tools",
@@ -160,7 +160,7 @@ def main():
160
160
  server = StdioMCPServer(
161
161
  tool_registry=registry,
162
162
  server_name="pomera-mcp-server",
163
- server_version="1.2.2"
163
+ server_version="1.2.4"
164
164
  )
165
165
 
166
166
  logger.info("Starting Pomera MCP Server...")
Binary file
Binary file
@@ -2,19 +2,23 @@
2
2
  /**
3
3
  * Pomera AI Commander - npm postinstall script
4
4
  *
5
- * This script runs after npm install/update and checks for
6
- * databases in the package directory that might be at risk.
7
- *
8
- * Displays a warning if portable mode data is detected.
5
+ * This script runs after npm install/update and:
6
+ * 1. Checks for databases in the package directory that might be at risk
7
+ * 2. Creates a desktop shortcut for easy access to the GUI
9
8
  */
10
9
 
11
10
  const fs = require('fs');
12
11
  const path = require('path');
12
+ const { execSync, spawn } = require('child_process');
13
+ const os = require('os');
13
14
 
14
15
  // Package root directory
15
16
  const packageDir = path.join(__dirname, '..');
16
17
 
17
- // Check for databases in package directory
18
+ // ============================================================
19
+ // Part 1: Database Warning Check
20
+ // ============================================================
21
+
18
22
  const databases = ['settings.db', 'notes.db', 'settings.json'];
19
23
  const foundDatabases = [];
20
24
 
@@ -30,7 +34,6 @@ databases.forEach(db => {
30
34
  }
31
35
  });
32
36
 
33
- // If databases found in package directory, show warning
34
37
  if (foundDatabases.length > 0) {
35
38
  console.log('\n' + '='.repeat(70));
36
39
  console.log('⚠️ POMERA DATA WARNING ⚠️');
@@ -56,7 +59,163 @@ if (foundDatabases.length > 0) {
56
59
  }
57
60
  console.log('\n' + '='.repeat(70) + '\n');
58
61
  } else {
59
- // No databases in package directory - safe configuration
60
62
  console.log('✅ Pomera AI Commander installed successfully.');
61
63
  console.log(' Data will be stored in platform-appropriate directory (safe from updates).');
62
64
  }
65
+
66
+ // ============================================================
67
+ // Part 2: Desktop Shortcut Creation
68
+ // ============================================================
69
+
70
+ function getDesktopPath() {
71
+ if (process.platform === 'win32') {
72
+ return path.join(os.homedir(), 'Desktop');
73
+ } else if (process.platform === 'darwin') {
74
+ return path.join(os.homedir(), 'Desktop');
75
+ } else {
76
+ // Linux - check XDG
77
+ const xdgDesktop = process.env.XDG_DESKTOP_DIR;
78
+ if (xdgDesktop) return xdgDesktop;
79
+ return path.join(os.homedir(), 'Desktop');
80
+ }
81
+ }
82
+
83
+ function createWindowsShortcut() {
84
+ const desktop = getDesktopPath();
85
+ const shortcutPath = path.join(desktop, 'Pomera AI Commander.lnk');
86
+ const pomeraPath = path.join(packageDir, 'pomera.py');
87
+ const iconPath = path.join(packageDir, 'resources', 'icon.ico');
88
+
89
+ let psScript = `
90
+ $WshShell = New-Object -ComObject WScript.Shell
91
+ $Shortcut = $WshShell.CreateShortcut("${shortcutPath.replace(/\\/g, '\\\\')}")
92
+ $Shortcut.TargetPath = "pythonw.exe"
93
+ $Shortcut.Arguments = '"${pomeraPath.replace(/\\/g, '\\\\')}"'
94
+ $Shortcut.WorkingDirectory = "${packageDir.replace(/\\/g, '\\\\')}"
95
+ $Shortcut.Description = "Pomera AI Commander - Text Processing Toolkit"
96
+ `;
97
+ if (fs.existsSync(iconPath)) {
98
+ psScript += `$Shortcut.IconLocation = "${iconPath.replace(/\\/g, '\\\\')},0"\n`;
99
+ }
100
+ psScript += '$Shortcut.Save()';
101
+
102
+ try {
103
+ execSync(`powershell -Command "${psScript.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
104
+ console.log(`\n🐕 Desktop shortcut created: ${shortcutPath}`);
105
+ return true;
106
+ } catch (e) {
107
+ console.log('\n⚠️ Could not create desktop shortcut automatically.');
108
+ console.log(' Run: python create_shortcut.py');
109
+ return false;
110
+ }
111
+ }
112
+
113
+ function createMacOSShortcut() {
114
+ const desktop = getDesktopPath();
115
+ const shortcutPath = path.join(desktop, 'Pomera AI Commander.command');
116
+ const pomeraPath = path.join(packageDir, 'pomera.py');
117
+
118
+ const script = `#!/bin/bash
119
+ # Pomera AI Commander Launcher
120
+ cd "${packageDir}"
121
+ python3 "${pomeraPath}"
122
+ `;
123
+
124
+ try {
125
+ fs.writeFileSync(shortcutPath, script);
126
+ fs.chmodSync(shortcutPath, 0o755);
127
+ console.log(`\n🐕 Desktop launcher created: ${shortcutPath}`);
128
+ return true;
129
+ } catch (e) {
130
+ console.log('\n⚠️ Could not create desktop shortcut automatically.');
131
+ return false;
132
+ }
133
+ }
134
+
135
+ function createLinuxShortcut() {
136
+ const desktop = getDesktopPath();
137
+ const shortcutPath = path.join(desktop, 'pomera-ai-commander.desktop');
138
+ const pomeraPath = path.join(packageDir, 'pomera.py');
139
+ const iconPath = path.join(packageDir, 'resources', 'icon.png');
140
+
141
+ const icon = fs.existsSync(iconPath) ? iconPath : 'utilities-terminal';
142
+
143
+ const desktopEntry = `[Desktop Entry]
144
+ Version=1.0
145
+ Type=Application
146
+ Name=Pomera AI Commander
147
+ Comment=Text Processing Toolkit with MCP tools for AI assistants
148
+ Exec=python3 "${pomeraPath}"
149
+ Icon=${icon}
150
+ Terminal=false
151
+ Categories=Development;Utility;TextTools;
152
+ StartupNotify=true
153
+ `;
154
+
155
+ try {
156
+ fs.writeFileSync(shortcutPath, desktopEntry);
157
+ fs.chmodSync(shortcutPath, 0o755);
158
+ console.log(`\n🐕 Desktop launcher created: ${shortcutPath}`);
159
+
160
+ // Also add to applications menu
161
+ const appsDir = path.join(os.homedir(), '.local', 'share', 'applications');
162
+ if (!fs.existsSync(appsDir)) {
163
+ fs.mkdirSync(appsDir, { recursive: true });
164
+ }
165
+ const appsPath = path.join(appsDir, 'pomera-ai-commander.desktop');
166
+ fs.writeFileSync(appsPath, desktopEntry);
167
+ console.log(` Also added to applications menu`);
168
+
169
+ return true;
170
+ } catch (e) {
171
+ console.log('\n⚠️ Could not create desktop shortcut automatically.');
172
+ return false;
173
+ }
174
+ }
175
+
176
+ function createDesktopShortcut() {
177
+ console.log('\n🐕 Creating desktop shortcut...');
178
+
179
+ const desktop = getDesktopPath();
180
+ if (!fs.existsSync(desktop)) {
181
+ console.log(` Desktop directory not found: ${desktop}`);
182
+ return false;
183
+ }
184
+
185
+ if (process.platform === 'win32') {
186
+ return createWindowsShortcut();
187
+ } else if (process.platform === 'darwin') {
188
+ return createMacOSShortcut();
189
+ } else {
190
+ return createLinuxShortcut();
191
+ }
192
+ }
193
+
194
+ // Check if Python/pythonw is available before creating shortcut
195
+ function checkPython() {
196
+ try {
197
+ if (process.platform === 'win32') {
198
+ execSync('where pythonw', { stdio: 'pipe' });
199
+ } else {
200
+ execSync('which python3', { stdio: 'pipe' });
201
+ }
202
+ return true;
203
+ } catch (e) {
204
+ return false;
205
+ }
206
+ }
207
+
208
+ // Create shortcut if Python is available
209
+ if (checkPython()) {
210
+ createDesktopShortcut();
211
+ } else {
212
+ console.log('\n⚠️ Python not found in PATH. Desktop shortcut not created.');
213
+ console.log(' Install Python and run: python create_shortcut.py');
214
+ }
215
+
216
+ console.log('\n📖 To start the GUI manually:');
217
+ console.log(' python pomera.py');
218
+ console.log('\n📖 To start the MCP server:');
219
+ console.log(' npx pomera-ai-commander');
220
+ console.log('');
221
+
@@ -99,6 +99,33 @@ class NotesWidget:
99
99
  if conn:
100
100
  conn.close()
101
101
 
102
+ def _sanitize_text(self, text: str) -> str:
103
+ """
104
+ Sanitize text by removing invalid UTF-8 surrogate characters.
105
+
106
+ Lone surrogates (U+D800 to U+DFFF) are invalid in UTF-8 and cause
107
+ encoding errors when saving to the database. This can happen when
108
+ pasting content from the clipboard that contains malformed data.
109
+
110
+ Args:
111
+ text: Input text that may contain invalid surrogates
112
+
113
+ Returns:
114
+ Sanitized text safe for UTF-8 encoding and database storage
115
+ """
116
+ if not text:
117
+ return text
118
+
119
+ try:
120
+ # This two-step process handles lone surrogates:
121
+ # 1. surrogatepass allows encoding surrogates (normally forbidden in UTF-8)
122
+ # 2. errors='replace' replaces invalid sequences with replacement char
123
+ sanitized = text.encode('utf-8', errors='surrogatepass').decode('utf-8', errors='replace')
124
+ return sanitized
125
+ except Exception:
126
+ # Fallback: manually filter out surrogate characters
127
+ return ''.join(c for c in text if not (0xD800 <= ord(c) <= 0xDFFF))
128
+
102
129
  def init_database(self) -> None:
103
130
  """Initialize the SQLite database and Full-Text Search (FTS5) table."""
104
131
  try:
@@ -707,10 +734,12 @@ class NotesWidget:
707
734
  row = conn.execute('SELECT * FROM notes WHERE id = ?', (self.current_item,)).fetchone()
708
735
  if row:
709
736
  now = datetime.now().isoformat()
737
+ # Sanitize text to prevent UTF-8 surrogate errors
710
738
  conn.execute('''
711
739
  INSERT INTO notes (Created, Modified, Title, Input, Output)
712
740
  VALUES (?, ?, ?, ?, ?)
713
- ''', (now, now, row['Title'], row['Input'], row['Output']))
741
+ ''', (now, now, self._sanitize_text(row['Title']),
742
+ self._sanitize_text(row['Input']), self._sanitize_text(row['Output'])))
714
743
  conn.commit()
715
744
  self.perform_search(select_first=True)
716
745
  self.logger.info(f"Duplicated note {self.current_item}")
@@ -788,9 +817,10 @@ class NotesWidget:
788
817
  return
789
818
 
790
819
  try:
791
- title = self.title_entry.get() if hasattr(self, 'title_entry') else ""
792
- input_content = self.input_display.get(1.0, tk.END).strip()
793
- output_content = self.output_display.get(1.0, tk.END).strip()
820
+ # Sanitize text to prevent UTF-8 surrogate errors from clipboard paste
821
+ title = self._sanitize_text(self.title_entry.get() if hasattr(self, 'title_entry') else "")
822
+ input_content = self._sanitize_text(self.input_display.get(1.0, tk.END).strip())
823
+ output_content = self._sanitize_text(self.output_display.get(1.0, tk.END).strip())
794
824
 
795
825
  now = datetime.now().isoformat()
796
826
 
@@ -962,12 +992,17 @@ class NotesWidget:
962
992
  The ID of the created note, or None on error
963
993
  """
964
994
  try:
995
+ # Sanitize text to prevent UTF-8 surrogate errors
996
+ sanitized_title = self._sanitize_text(title)
997
+ sanitized_input = self._sanitize_text(input_content)
998
+ sanitized_output = self._sanitize_text(output_content)
999
+
965
1000
  now = datetime.now().isoformat()
966
1001
  with self.get_db_connection() as conn:
967
1002
  cursor = conn.execute('''
968
1003
  INSERT INTO notes (Created, Modified, Title, Input, Output)
969
1004
  VALUES (?, ?, ?, ?, ?)
970
- ''', (now, now, title, input_content, output_content))
1005
+ ''', (now, now, sanitized_title, sanitized_input, sanitized_output))
971
1006
  note_id = cursor.lastrowid
972
1007
  conn.commit()
973
1008