pomera-ai-commander 1.2.1 → 1.2.2

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,127 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-Update Migration Script for Pomera AI Commander
4
+
5
+ Run this script BEFORE updating via npm or pip to migrate your databases
6
+ to the platform-appropriate user data directory.
7
+
8
+ Usage:
9
+ python migrate_data.py
10
+
11
+ This will:
12
+ 1. Find existing databases in the installation directory
13
+ 2. Copy them to the user data directory (safe from updates)
14
+ 3. Verify the migration was successful
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import shutil
20
+ from pathlib import Path
21
+
22
+ def get_user_data_dir():
23
+ """Get platform-appropriate user data directory."""
24
+ import platform
25
+ system = platform.system()
26
+ app_name = "Pomera-AI-Commander"
27
+
28
+ try:
29
+ from platformdirs import user_data_dir
30
+ return Path(user_data_dir(app_name, "PomeraAI"))
31
+ except ImportError:
32
+ pass
33
+
34
+ if system == "Windows":
35
+ base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~"))
36
+ return Path(base) / app_name
37
+ elif system == "Darwin":
38
+ return Path.home() / "Library" / "Application Support" / app_name
39
+ else:
40
+ xdg_data = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
41
+ return Path(xdg_data) / app_name
42
+
43
+ def get_installation_dir():
44
+ """Get the current installation directory."""
45
+ return Path(__file__).parent
46
+
47
+ def migrate():
48
+ """Migrate databases from installation dir to user data dir."""
49
+ install_dir = get_installation_dir()
50
+ user_dir = get_user_data_dir()
51
+
52
+ print(f"Installation directory: {install_dir}")
53
+ print(f"User data directory: {user_dir}")
54
+ print()
55
+
56
+ # Create user data directory
57
+ user_dir.mkdir(parents=True, exist_ok=True)
58
+
59
+ databases = ['settings.db', 'notes.db', 'settings.json']
60
+ migrated = []
61
+ skipped = []
62
+ not_found = []
63
+
64
+ for db_name in databases:
65
+ src = install_dir / db_name
66
+ dst = user_dir / db_name
67
+
68
+ if not src.exists():
69
+ not_found.append(db_name)
70
+ continue
71
+
72
+ if dst.exists():
73
+ src_size = src.stat().st_size
74
+ dst_size = dst.stat().st_size
75
+ print(f"⚠️ {db_name}: Already exists in target")
76
+ print(f" Source size: {src_size:,} bytes")
77
+ print(f" Target size: {dst_size:,} bytes")
78
+
79
+ if src_size > dst_size:
80
+ response = input(f" Source is larger. Overwrite target? [y/N]: ")
81
+ if response.lower() == 'y':
82
+ shutil.copy2(src, dst)
83
+ migrated.append(db_name)
84
+ print(f" ✅ Overwritten")
85
+ else:
86
+ skipped.append(db_name)
87
+ print(f" ⏭️ Skipped")
88
+ else:
89
+ skipped.append(db_name)
90
+ print(f" ⏭️ Keeping existing (same or larger)")
91
+ else:
92
+ shutil.copy2(src, dst)
93
+ migrated.append(db_name)
94
+ print(f"✅ {db_name}: Migrated ({src.stat().st_size:,} bytes)")
95
+
96
+ print()
97
+ print("=" * 50)
98
+ print("Migration Summary")
99
+ print("=" * 50)
100
+
101
+ if migrated:
102
+ print(f"✅ Migrated: {', '.join(migrated)}")
103
+ if skipped:
104
+ print(f"⏭️ Skipped: {', '.join(skipped)}")
105
+ if not_found:
106
+ print(f"➖ Not found: {', '.join(not_found)}")
107
+
108
+ print()
109
+ print(f"Your data is now safe in: {user_dir}")
110
+ print()
111
+ print("You can now safely update Pomera via npm or pip!")
112
+
113
+ return len(migrated) > 0 or len(skipped) > 0
114
+
115
+ if __name__ == "__main__":
116
+ print("=" * 50)
117
+ print("Pomera AI Commander - Pre-Update Migration")
118
+ print("=" * 50)
119
+ print()
120
+
121
+ if migrate():
122
+ print("✅ Migration complete!")
123
+ sys.exit(0)
124
+ else:
125
+ print("ℹ️ No databases found to migrate.")
126
+ print(" (This is normal for fresh installations)")
127
+ sys.exit(0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pomera-ai-commander",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
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": {
@@ -11,7 +11,8 @@
11
11
  "start": "python pomera_mcp_server.py",
12
12
  "mcp": "python pomera_mcp_server.py",
13
13
  "list-tools": "python pomera_mcp_server.py --list-tools",
14
- "test": "python -m pytest"
14
+ "test": "python -m pytest",
15
+ "postinstall": "node scripts/postinstall.js"
15
16
  },
16
17
  "keywords": [
17
18
  "mcp",
@@ -48,8 +49,10 @@
48
49
  "bin/",
49
50
  "core/",
50
51
  "tools/",
52
+ "scripts/",
51
53
  "pomera_mcp_server.py",
52
54
  "pomera.py",
55
+ "migrate_data.py",
53
56
  "mcp.json",
54
57
  "README.md",
55
58
  "LICENSE",
package/pomera.py CHANGED
@@ -59,6 +59,18 @@ except ImportError:
59
59
  DATABASE_SETTINGS_AVAILABLE = False
60
60
  print("Database Settings Manager not available, falling back to JSON")
61
61
 
62
+ # Data Directory module import (cross-platform data storage)
63
+ try:
64
+ from core.data_directory import (
65
+ get_database_path, get_backup_dir, migrate_legacy_databases,
66
+ is_portable_mode, get_data_directory_info, check_portable_mode_warning,
67
+ check_and_execute_pending_migration, set_custom_data_directory, load_config
68
+ )
69
+ DATA_DIRECTORY_AVAILABLE = True
70
+ except ImportError:
71
+ DATA_DIRECTORY_AVAILABLE = False
72
+ print("Data Directory module not available, using legacy paths")
73
+
62
74
  # Case Tool module import
63
75
  try:
64
76
  from tools.case_tool import CaseTool
@@ -527,7 +539,24 @@ else:
527
539
  self.linenumbers.bind("<Button-5>", self._on_mousewheel)
528
540
  self.text.bind("<<Modified>>", self._on_text_modified)
529
541
  self.text.bind("<Configure>", self._on_text_modified)
542
+
543
+ # Paste events - insert undo separator after paste
544
+ self.text.bind("<<Paste>>", self._on_paste)
545
+ self.text.bind("<Control-v>", self._on_paste)
546
+ self.text.bind("<Control-V>", self._on_paste)
547
+ self.text.bind("<Shift-Insert>", self._on_paste)
548
+
530
549
  self._on_text_modified()
550
+
551
+ def _on_paste(self, event=None):
552
+ """Insert undo separator after paste to separate from subsequent typing."""
553
+ def insert_separator():
554
+ try:
555
+ self.text.edit_separator()
556
+ except Exception:
557
+ pass
558
+ self.after(10, insert_separator)
559
+
531
560
 
532
561
  def _on_text_scroll(self, *args):
533
562
  self.text.yview(*args)
@@ -582,9 +611,7 @@ class PromeraAISettingsManager:
582
611
  def get_pattern_library(self) -> List[Dict[str, str]]:
583
612
  """Get the regex pattern library."""
584
613
  # Initialize pattern library if it doesn't exist
585
- if ("pattern_library" not in self.app.settings or
586
- len(self.app.settings.get("pattern_library", [])) < 10):
587
-
614
+ if "pattern_library" not in self.app.settings:
588
615
  # Try to import and use the comprehensive pattern library
589
616
  try:
590
617
  from core.regex_pattern_library import RegexPatternLibrary
@@ -629,6 +656,11 @@ class PromeraAISettingsManager:
629
656
  self.app.logger.info(f"Added {added_count} common extraction patterns to pattern library")
630
657
 
631
658
  return pattern_library
659
+
660
+ def set_pattern_library(self, pattern_library: List[Dict[str, str]]):
661
+ """Explicitly update and persist the pattern library."""
662
+ self.app.settings["pattern_library"] = pattern_library
663
+ self.app.save_settings()
632
664
 
633
665
 
634
666
  class DialogSettingsAdapter:
@@ -748,7 +780,7 @@ class PromeraAIApp(tk.Tk):
748
780
  self.title("Pomera AI Commander")
749
781
  self.geometry(AppConfig.DEFAULT_WINDOW_SIZE)
750
782
 
751
- self._after_id = None
783
+ self._after_id = None
752
784
  self._regex_cache = {}
753
785
  self.ai_widgets = {}
754
786
  self.ai_provider_urls = {
@@ -775,10 +807,30 @@ class PromeraAIApp(tk.Tk):
775
807
  # Initialize database settings manager or fallback to JSON
776
808
  if DATABASE_SETTINGS_AVAILABLE:
777
809
  try:
810
+ # Check for and execute any pending data migration FIRST
811
+ # (before any database connections are opened)
812
+ if DATA_DIRECTORY_AVAILABLE:
813
+ migration_msg = check_and_execute_pending_migration()
814
+ if migration_msg:
815
+ print(f"Data migration completed: {migration_msg}")
816
+
817
+ # Migrate legacy databases if needed (console INFO log only)
818
+ migrate_legacy_databases()
819
+ db_path = get_database_path("settings.db")
820
+ backup_path = str(get_backup_dir() / "settings_backup.db")
821
+ json_path = get_database_path("settings.json")
822
+ if is_portable_mode():
823
+ print("Running in portable mode - data stored in installation directory")
824
+ else:
825
+ # Fallback to legacy relative paths
826
+ db_path = "settings.db"
827
+ backup_path = "settings_backup.db"
828
+ json_path = "settings.json"
829
+
778
830
  self.db_settings_manager = DatabaseSettingsManager(
779
- db_path="settings.db",
780
- backup_path="settings_backup.db",
781
- json_settings_path="settings.json"
831
+ db_path=db_path,
832
+ backup_path=backup_path,
833
+ json_settings_path=json_path
782
834
  )
783
835
  # Provide default settings to database manager
784
836
  self.db_settings_manager.set_default_settings_provider(self._get_default_settings)
@@ -897,9 +949,10 @@ class PromeraAIApp(tk.Tk):
897
949
  if CURL_TOOL_MODULE_AVAILABLE:
898
950
  self.bind_all("<Control-u>", lambda e: self.open_curl_tool_window())
899
951
 
900
- # Set up Notes widget keyboard shortcut (Ctrl+S for Save as Note)
952
+ # Set up Notes widget keyboard shortcuts
901
953
  if NOTES_WIDGET_MODULE_AVAILABLE:
902
- self.bind_all("<Control-s>", lambda e: self.save_as_note())
954
+ self.bind_all("<Control-s>", lambda e: self.save_as_note()) # Ctrl+S for Save as Note
955
+ self.bind_all("<Control-n>", lambda e: self.open_notes_widget()) # Ctrl+N for Notes window
903
956
 
904
957
  # Set up MCP Manager keyboard shortcut (Ctrl+M)
905
958
  if MCP_MANAGER_MODULE_AVAILABLE:
@@ -968,6 +1021,348 @@ class PromeraAIApp(tk.Tk):
968
1021
 
969
1022
  self.logger.info("Background maintenance tasks scheduled (including auto-backup)")
970
1023
 
1024
+ def _show_data_location_dialog(self):
1025
+ """Show the Data Location settings dialog."""
1026
+ from tkinter import filedialog
1027
+
1028
+ dialog = tk.Toplevel(self)
1029
+ dialog.title("Data Location Settings")
1030
+ dialog.withdraw() # Hide until centered
1031
+ dialog.transient(self)
1032
+
1033
+ # Get current data info
1034
+ if DATA_DIRECTORY_AVAILABLE:
1035
+ data_info = get_data_directory_info()
1036
+ current_path = data_info.get('user_data_dir', 'Unknown')
1037
+ portable = data_info.get('portable_mode', False)
1038
+ config = load_config()
1039
+ custom_path = config.get('data_directory')
1040
+ else:
1041
+ current_path = "Data directory module not available"
1042
+ portable = False
1043
+ custom_path = None
1044
+
1045
+ # Main frame with padding
1046
+ main_frame = ttk.Frame(dialog, padding="20")
1047
+ main_frame.pack(fill=tk.BOTH, expand=True)
1048
+
1049
+ # Current location
1050
+ ttk.Label(main_frame, text="Current Data Location:", font=('TkDefaultFont', 10, 'bold')).pack(anchor=tk.W)
1051
+
1052
+ path_frame = ttk.Frame(main_frame)
1053
+ path_frame.pack(fill=tk.X, pady=(5, 15))
1054
+
1055
+ path_entry = ttk.Entry(path_frame, width=60)
1056
+ path_entry.insert(0, current_path)
1057
+ path_entry.config(state='readonly')
1058
+ path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
1059
+
1060
+ # Mode selection
1061
+ ttk.Label(main_frame, text="Data Storage Mode:", font=('TkDefaultFont', 10, 'bold')).pack(anchor=tk.W, pady=(10, 5))
1062
+
1063
+ mode_var = tk.StringVar()
1064
+ if custom_path:
1065
+ mode_var.set("custom")
1066
+ elif portable:
1067
+ mode_var.set("portable")
1068
+ else:
1069
+ mode_var.set("platform")
1070
+
1071
+ custom_path_var = tk.StringVar(value=custom_path or "")
1072
+
1073
+ # Platform default option
1074
+ platform_frame = ttk.Frame(main_frame)
1075
+ platform_frame.pack(fill=tk.X, pady=2)
1076
+ ttk.Radiobutton(platform_frame, text="Use platform default (recommended)",
1077
+ variable=mode_var, value="platform").pack(anchor=tk.W)
1078
+
1079
+ # Custom location option
1080
+ custom_frame = ttk.Frame(main_frame)
1081
+ custom_frame.pack(fill=tk.X, pady=2)
1082
+ ttk.Radiobutton(custom_frame, text="Use custom location:",
1083
+ variable=mode_var, value="custom").pack(side=tk.LEFT)
1084
+
1085
+ custom_entry = ttk.Entry(custom_frame, textvariable=custom_path_var, width=40)
1086
+ custom_entry.pack(side=tk.LEFT, padx=5)
1087
+
1088
+ def browse_folder():
1089
+ folder = filedialog.askdirectory(title="Select Data Directory")
1090
+ if folder:
1091
+ custom_path_var.set(folder)
1092
+ mode_var.set("custom")
1093
+
1094
+ ttk.Button(custom_frame, text="Browse...", command=browse_folder).pack(side=tk.LEFT)
1095
+
1096
+ # Portable mode option
1097
+ portable_frame = ttk.Frame(main_frame)
1098
+ portable_frame.pack(fill=tk.X, pady=2)
1099
+ ttk.Radiobutton(portable_frame, text="Portable mode (store in app folder - NOT RECOMMENDED)",
1100
+ variable=mode_var, value="portable").pack(anchor=tk.W)
1101
+
1102
+ # Warning for portable mode
1103
+ warning_frame = ttk.Frame(main_frame)
1104
+ warning_frame.pack(fill=tk.X, pady=(15, 10))
1105
+
1106
+ warning_label = ttk.Label(warning_frame,
1107
+ text="⚠️ Warning: Portable mode data will be LOST during npm/pip updates!",
1108
+ foreground='red')
1109
+
1110
+ def update_warning(*args):
1111
+ if mode_var.get() == "portable":
1112
+ warning_label.pack(anchor=tk.W)
1113
+ else:
1114
+ warning_label.pack_forget()
1115
+
1116
+ mode_var.trace_add('write', update_warning)
1117
+ update_warning()
1118
+
1119
+ # Migrate checkbox
1120
+ migrate_var = tk.BooleanVar(value=True)
1121
+ ttk.Checkbutton(main_frame, text="Migrate existing data to new location",
1122
+ variable=migrate_var).pack(anchor=tk.W, pady=(10, 0))
1123
+
1124
+ # Buttons
1125
+ button_frame = ttk.Frame(main_frame)
1126
+ button_frame.pack(fill=tk.X, pady=(20, 0))
1127
+
1128
+ def apply_changes():
1129
+ mode = mode_var.get()
1130
+ migrate = migrate_var.get()
1131
+
1132
+ if mode == "platform":
1133
+ new_path = None
1134
+ elif mode == "custom":
1135
+ new_path = custom_path_var.get()
1136
+ if not new_path:
1137
+ messagebox.showerror("Error", "Please specify a custom directory path.")
1138
+ return
1139
+ else: # portable
1140
+ # For portable mode, set to installation directory
1141
+ if DATA_DIRECTORY_AVAILABLE:
1142
+ from core.data_directory import _get_installation_dir
1143
+ new_path = str(_get_installation_dir())
1144
+ else:
1145
+ messagebox.showerror("Error", "Portable mode not available.")
1146
+ return
1147
+
1148
+ if DATA_DIRECTORY_AVAILABLE:
1149
+ result = set_custom_data_directory(new_path, migrate=migrate)
1150
+
1151
+ if result.get("restart_required"):
1152
+ dialog.destroy()
1153
+ response = messagebox.askyesno(
1154
+ "Restart Required",
1155
+ "Data location changed. The application needs to restart to migrate data.\n\n"
1156
+ "Close the application now?",
1157
+ icon='warning'
1158
+ )
1159
+ if response:
1160
+ self.on_closing()
1161
+ else:
1162
+ dialog.destroy()
1163
+ messagebox.showinfo("Success", result.get("message", "Settings saved."))
1164
+ else:
1165
+ messagebox.showerror("Error", "Data directory module not available.")
1166
+
1167
+ ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
1168
+ ttk.Button(button_frame, text="Apply & Restart", command=apply_changes).pack(side=tk.RIGHT)
1169
+
1170
+ # Center and show dialog (after all widgets created to avoid blink)
1171
+ dialog.update_idletasks()
1172
+ dialog.geometry("550x380")
1173
+ x = self.winfo_x() + (self.winfo_width() - 550) // 2
1174
+ y = self.winfo_y() + (self.winfo_height() - 380) // 2
1175
+ dialog.geometry(f"550x380+{x}+{y}")
1176
+ dialog.deiconify()
1177
+ dialog.grab_set()
1178
+
1179
+ def _show_update_dialog(self):
1180
+ """Show the Check for Updates dialog with GitHub version check."""
1181
+ import webbrowser
1182
+ import urllib.request
1183
+ import json
1184
+ import platform
1185
+
1186
+ dialog = tk.Toplevel(self)
1187
+ dialog.title("Check for Updates")
1188
+ dialog.withdraw() # Hide until centered
1189
+ dialog.transient(self)
1190
+
1191
+ main_frame = ttk.Frame(dialog, padding="20")
1192
+ main_frame.pack(fill=tk.BOTH, expand=True)
1193
+
1194
+ ttk.Label(main_frame, text="Pomera AI Commander", font=('TkDefaultFont', 14, 'bold')).pack(pady=(0, 10))
1195
+
1196
+ # Get current version - try pyproject.toml first (for local dev), then importlib.metadata
1197
+ current_version = None
1198
+ try:
1199
+ # Try reading from pyproject.toml (when running from source)
1200
+ import re
1201
+ pyproject_path = Path(__file__).parent / "pyproject.toml"
1202
+ if pyproject_path.exists():
1203
+ content = pyproject_path.read_text()
1204
+ match = re.search(r'version = "([^"]+)"', content)
1205
+ if match:
1206
+ current_version = match.group(1)
1207
+ except Exception:
1208
+ pass
1209
+
1210
+ if not current_version:
1211
+ try:
1212
+ import importlib.metadata
1213
+ current_version = importlib.metadata.version("pomera-ai-commander")
1214
+ except Exception:
1215
+ current_version = "Unknown"
1216
+
1217
+ # Detect OS for download link
1218
+ system = platform.system()
1219
+ if system == "Windows":
1220
+ os_pattern = "windows"
1221
+ os_name = "Windows"
1222
+ elif system == "Darwin":
1223
+ os_pattern = "macos"
1224
+ os_name = "macOS"
1225
+ else:
1226
+ os_pattern = "linux"
1227
+ os_name = "Linux"
1228
+
1229
+ # Fetch latest version from GitHub
1230
+ latest_version = None
1231
+ update_available = False
1232
+ download_url = None
1233
+ release_data = None
1234
+
1235
+ try:
1236
+ url = "https://api.github.com/repos/matbanik/Pomera-AI-Commander/releases/latest"
1237
+ req = urllib.request.Request(url, headers={'User-Agent': 'Pomera-AI-Commander'})
1238
+ with urllib.request.urlopen(req, timeout=5) as response:
1239
+ release_data = json.loads(response.read().decode())
1240
+ latest_version = release_data.get('tag_name', '').lstrip('v')
1241
+
1242
+ # Compare versions
1243
+ if current_version != "Unknown" and latest_version:
1244
+ try:
1245
+ current_parts = [int(x) for x in current_version.split('.')]
1246
+ latest_parts = [int(x) for x in latest_version.split('.')]
1247
+ update_available = latest_parts > current_parts
1248
+ except ValueError:
1249
+ update_available = current_version != latest_version
1250
+
1251
+ # Find platform-specific download URL
1252
+ assets = release_data.get('assets', [])
1253
+ for asset in assets:
1254
+ name = asset.get('name', '').lower()
1255
+ if os_pattern in name and (name.endswith('.exe') or name.endswith('.zip') or name.endswith('.tar.gz')):
1256
+ download_url = asset.get('browser_download_url')
1257
+ break
1258
+ except Exception as e:
1259
+ latest_version = f"Unable to check ({type(e).__name__})"
1260
+
1261
+ # Version info frame
1262
+ version_frame = ttk.Frame(main_frame)
1263
+ version_frame.pack(fill=tk.X, pady=(0, 15))
1264
+
1265
+ ttk.Label(version_frame, text=f"Current Version: {current_version}").pack(anchor=tk.W)
1266
+
1267
+ if latest_version and not latest_version.startswith("Unable"):
1268
+ if update_available:
1269
+ # Update available - BOLD BLACK
1270
+ ttk.Label(version_frame, text=f"Latest Version: {latest_version}",
1271
+ font=('TkDefaultFont', 11, 'bold')).pack(anchor=tk.W)
1272
+ ttk.Label(version_frame, text="⬆️ Update available!",
1273
+ font=('TkDefaultFont', 11, 'bold')).pack(anchor=tk.W, pady=(5, 0))
1274
+ else:
1275
+ # Up to date - GREEN
1276
+ ttk.Label(version_frame, text=f"Latest Version: {latest_version}",
1277
+ foreground='green').pack(anchor=tk.W)
1278
+ ttk.Label(version_frame, text="✓ You're up to date!",
1279
+ foreground='green', font=('TkDefaultFont', 10, 'bold')).pack(anchor=tk.W, pady=(5, 0))
1280
+ elif latest_version:
1281
+ ttk.Label(version_frame, text=f"Latest Version: {latest_version}", foreground='orange').pack(anchor=tk.W)
1282
+
1283
+ # Data location info
1284
+ if DATA_DIRECTORY_AVAILABLE:
1285
+ data_info = get_data_directory_info()
1286
+ mode = "Portable" if data_info.get('portable_mode') else "Platform"
1287
+ ttk.Label(main_frame, text=f"Installation Mode: {mode}").pack(anchor=tk.W)
1288
+ ttk.Label(main_frame, text=f"Data Location: {data_info.get('user_data_dir', 'Unknown')}",
1289
+ wraplength=500).pack(anchor=tk.W, pady=(0, 10))
1290
+
1291
+ # Warning for portable mode
1292
+ if data_info.get('portable_mode'):
1293
+ warning_frame = ttk.Frame(main_frame)
1294
+ warning_frame.pack(fill=tk.X, pady=5)
1295
+ ttk.Label(warning_frame,
1296
+ text="⚠️ PORTABLE MODE: Backup your data before updating!",
1297
+ foreground='red', font=('TkDefaultFont', 10, 'bold')).pack()
1298
+
1299
+ # Download link if update available
1300
+ if update_available and download_url:
1301
+ download_frame = ttk.Frame(main_frame)
1302
+ download_frame.pack(fill=tk.X, pady=(10, 0))
1303
+ ttk.Label(download_frame, text=f"📥 {os_name} Download:", font=('TkDefaultFont', 10, 'bold')).pack(anchor=tk.W)
1304
+
1305
+ # Create a clickable link-style label
1306
+ link_label = ttk.Label(download_frame, text=download_url.split('/')[-1],
1307
+ foreground='blue', cursor='hand2', font=('TkDefaultFont', 9, 'underline'))
1308
+ link_label.pack(anchor=tk.W)
1309
+ link_label.bind('<Button-1>', lambda e: webbrowser.open(download_url))
1310
+
1311
+ # Buttons
1312
+ button_frame = ttk.Frame(main_frame)
1313
+ button_frame.pack(fill=tk.X, pady=(15, 0))
1314
+
1315
+ def open_releases():
1316
+ webbrowser.open("https://github.com/matbanik/Pomera-AI-Commander/releases")
1317
+
1318
+ def download_now():
1319
+ if download_url:
1320
+ webbrowser.open(download_url)
1321
+
1322
+ def create_backup():
1323
+ if DATA_DIRECTORY_AVAILABLE:
1324
+ from core.data_directory import check_portable_mode_warning
1325
+ warning_info = check_portable_mode_warning(show_console_warning=False)
1326
+ if warning_info:
1327
+ # Create backup
1328
+ from core.mcp.tool_registry import ToolRegistry
1329
+ registry = ToolRegistry(register_builtins=False)
1330
+ registry._register_safe_update_tool()
1331
+ result = registry.execute('pomera_safe_update', {'action': 'backup'})
1332
+ messagebox.showinfo("Backup", f"Backup created!\n\n{result.content[0]['text'] if result.content else 'Check Documents/pomera-backup folder'}")
1333
+ else:
1334
+ messagebox.showinfo("Backup", "No backup needed - your data is safely stored in platform directories.")
1335
+
1336
+ if update_available and download_url:
1337
+ ttk.Button(button_frame, text=f"Download for {os_name}", command=download_now).pack(side=tk.LEFT, padx=5)
1338
+ ttk.Button(button_frame, text="Create Backup", command=create_backup).pack(side=tk.LEFT, padx=5)
1339
+ ttk.Button(button_frame, text="All Releases", command=open_releases).pack(side=tk.LEFT, padx=5)
1340
+ ttk.Button(button_frame, text="Close", command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
1341
+
1342
+ # Center and show dialog (after all widgets created to avoid blink)
1343
+ dialog.update_idletasks()
1344
+ dialog.geometry("550x400")
1345
+ x = self.winfo_x() + (self.winfo_width() - 550) // 2
1346
+ y = self.winfo_y() + (self.winfo_height() - 400) // 2
1347
+ dialog.geometry(f"550x400+{x}+{y}")
1348
+ dialog.deiconify()
1349
+ dialog.grab_set()
1350
+
1351
+ def _show_about_dialog(self):
1352
+ """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"
1358
+
1359
+ messagebox.showinfo(
1360
+ "About Pomera AI Commander",
1361
+ f"Pomera AI Commander v{version}\n\n"
1362
+ "Text processing toolkit with MCP tools for AI assistants.\n\n"
1363
+ "https://github.com/matbanik/Pomera-AI-Commander"
1364
+ )
1365
+
971
1366
  def _auto_save_settings(self):
972
1367
  """Auto-save settings periodically (called by Task Scheduler)."""
973
1368
  try:
@@ -2604,6 +2999,7 @@ class PromeraAIApp(tk.Tk):
2604
2999
  # File Menu
2605
3000
  file_menu = tk.Menu(menubar, tearoff=0)
2606
3001
  menubar.add_cascade(label="File", menu=file_menu)
3002
+ file_menu.add_command(label="Data Location...", command=self._show_data_location_dialog)
2607
3003
  file_menu.add_command(label="Export Location", command=self.browse_export_path)
2608
3004
  file_menu.add_separator()
2609
3005
 
@@ -2694,7 +3090,8 @@ class PromeraAIApp(tk.Tk):
2694
3090
  if NOTES_WIDGET_MODULE_AVAILABLE:
2695
3091
  widgets_menu.add_command(
2696
3092
  label="Notes",
2697
- command=self.open_notes_widget
3093
+ command=self.open_notes_widget,
3094
+ accelerator="Ctrl+N"
2698
3095
  )
2699
3096
 
2700
3097
  # Add MCP Manager if available
@@ -2708,9 +3105,13 @@ class PromeraAIApp(tk.Tk):
2708
3105
  # Help Menu
2709
3106
  help_menu = tk.Menu(menubar, tearoff=0)
2710
3107
  menubar.add_cascade(label="Help", menu=help_menu)
3108
+ help_menu.add_command(label="Check for Updates...", command=self._show_update_dialog)
3109
+ help_menu.add_separator()
2711
3110
  help_menu.add_command(label="GitHub", command=lambda: webbrowser.open_new("https://github.com/matbanik/Pomera-AI-Commander"))
2712
3111
  help_menu.add_command(label="Report Issue", command=self.open_report_issue)
2713
3112
  help_menu.add_command(label="Ask AI", command=self.open_ai_tools)
3113
+ help_menu.add_separator()
3114
+ help_menu.add_command(label="About", command=self._show_about_dialog)
2714
3115
 
2715
3116
  def open_report_issue(self):
2716
3117
  """Opens the GitHub issues page in the default browser."""
@@ -70,7 +70,7 @@ def main():
70
70
  parser.add_argument(
71
71
  "--version",
72
72
  action="version",
73
- version="pomera-mcp-server 1.1.1"
73
+ version="pomera-mcp-server 1.2.2"
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.1.1"
163
+ server_version="1.2.2"
164
164
  )
165
165
 
166
166
  logger.info("Starting Pomera MCP Server...")
package/requirements.txt CHANGED
@@ -3,6 +3,7 @@ requests>=2.25.0
3
3
  reportlab>=3.6.0
4
4
  python-docx>=0.8.11
5
5
  aiohttp>=3.9.0
6
+ platformdirs>=4.0.0
6
7
 
7
8
  # AI Tools dependencies
8
9
  huggingface-hub>=0.16.0