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.
- package/core/backup_recovery_manager.py +169 -3
- package/core/data_directory.py +549 -0
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +30 -0
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/tool_registry.py +330 -3
- package/core/memento.py +275 -0
- package/mcp.json +1 -1
- package/migrate_data.py +127 -0
- package/package.json +5 -2
- package/pomera.py +411 -10
- package/pomera_mcp_server.py +2 -2
- package/requirements.txt +1 -0
- package/scripts/Dockerfile.alpine +43 -0
- package/scripts/Dockerfile.gui-test +54 -0
- package/scripts/Dockerfile.linux +43 -0
- package/scripts/Dockerfile.test-linux +80 -0
- package/scripts/Dockerfile.ubuntu +39 -0
- package/scripts/README.md +53 -0
- package/scripts/build-all.bat +113 -0
- package/scripts/build-docker.bat +53 -0
- package/scripts/build-docker.sh +55 -0
- package/scripts/build-optimized.bat +101 -0
- package/scripts/build.sh +78 -0
- package/scripts/docker-compose.test.yml +27 -0
- package/scripts/docker-compose.yml +32 -0
- package/scripts/postinstall.js +62 -0
- package/scripts/requirements-minimal.txt +33 -0
- package/scripts/test-linux-simple.bat +28 -0
- package/scripts/validate-release-workflow.py +450 -0
- package/tools/diff_viewer.py +797 -52
- package/tools/find_replace.py +551 -12
- package/tools/notes_widget.py +8 -3
- package/tools/regex_extractor.py +5 -5
package/migrate_data.py
ADDED
|
@@ -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.
|
|
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
|
|
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=
|
|
780
|
-
backup_path=
|
|
781
|
-
json_settings_path=
|
|
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
|
|
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."""
|
package/pomera_mcp_server.py
CHANGED
|
@@ -70,7 +70,7 @@ def main():
|
|
|
70
70
|
parser.add_argument(
|
|
71
71
|
"--version",
|
|
72
72
|
action="version",
|
|
73
|
-
version="pomera-mcp-server 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.
|
|
163
|
+
server_version="1.2.2"
|
|
164
164
|
)
|
|
165
165
|
|
|
166
166
|
logger.info("Starting Pomera MCP Server...")
|