pomera-ai-commander 1.2.1 → 1.2.3
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 +369 -9
- 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 +408 -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 +48 -8
- package/tools/regex_extractor.py +5 -5
package/tools/notes_widget.py
CHANGED
|
@@ -47,9 +47,14 @@ class NotesWidget:
|
|
|
47
47
|
self.send_to_input_callback = send_to_input_callback
|
|
48
48
|
self.dialog_manager = dialog_manager
|
|
49
49
|
|
|
50
|
-
# Database path - use
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
# Database path - use platform-appropriate data directory
|
|
51
|
+
try:
|
|
52
|
+
from core.data_directory import get_database_path
|
|
53
|
+
self.db_path = get_database_path('notes.db')
|
|
54
|
+
except ImportError:
|
|
55
|
+
# Fallback to legacy behavior - same directory as Pomera
|
|
56
|
+
db_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
57
|
+
self.db_path = os.path.join(db_dir, 'notes.db')
|
|
53
58
|
|
|
54
59
|
# State management
|
|
55
60
|
self.search_debounce_timer: Optional[str] = None
|
|
@@ -94,6 +99,33 @@ class NotesWidget:
|
|
|
94
99
|
if conn:
|
|
95
100
|
conn.close()
|
|
96
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
|
+
|
|
97
129
|
def init_database(self) -> None:
|
|
98
130
|
"""Initialize the SQLite database and Full-Text Search (FTS5) table."""
|
|
99
131
|
try:
|
|
@@ -702,10 +734,12 @@ class NotesWidget:
|
|
|
702
734
|
row = conn.execute('SELECT * FROM notes WHERE id = ?', (self.current_item,)).fetchone()
|
|
703
735
|
if row:
|
|
704
736
|
now = datetime.now().isoformat()
|
|
737
|
+
# Sanitize text to prevent UTF-8 surrogate errors
|
|
705
738
|
conn.execute('''
|
|
706
739
|
INSERT INTO notes (Created, Modified, Title, Input, Output)
|
|
707
740
|
VALUES (?, ?, ?, ?, ?)
|
|
708
|
-
''', (now, now, row['Title'],
|
|
741
|
+
''', (now, now, self._sanitize_text(row['Title']),
|
|
742
|
+
self._sanitize_text(row['Input']), self._sanitize_text(row['Output'])))
|
|
709
743
|
conn.commit()
|
|
710
744
|
self.perform_search(select_first=True)
|
|
711
745
|
self.logger.info(f"Duplicated note {self.current_item}")
|
|
@@ -783,9 +817,10 @@ class NotesWidget:
|
|
|
783
817
|
return
|
|
784
818
|
|
|
785
819
|
try:
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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())
|
|
789
824
|
|
|
790
825
|
now = datetime.now().isoformat()
|
|
791
826
|
|
|
@@ -957,12 +992,17 @@ class NotesWidget:
|
|
|
957
992
|
The ID of the created note, or None on error
|
|
958
993
|
"""
|
|
959
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
|
+
|
|
960
1000
|
now = datetime.now().isoformat()
|
|
961
1001
|
with self.get_db_connection() as conn:
|
|
962
1002
|
cursor = conn.execute('''
|
|
963
1003
|
INSERT INTO notes (Created, Modified, Title, Input, Output)
|
|
964
1004
|
VALUES (?, ?, ?, ?, ?)
|
|
965
|
-
''', (now, now,
|
|
1005
|
+
''', (now, now, sanitized_title, sanitized_input, sanitized_output))
|
|
966
1006
|
note_id = cursor.lastrowid
|
|
967
1007
|
conn.commit()
|
|
968
1008
|
|
package/tools/regex_extractor.py
CHANGED
|
@@ -350,7 +350,7 @@ class RegexExtractorUI:
|
|
|
350
350
|
new_item_id = len(pattern_library) - 1
|
|
351
351
|
tree.selection_set(str(new_item_id))
|
|
352
352
|
tree.focus(str(new_item_id))
|
|
353
|
-
self.settings_manager.save_settings()
|
|
353
|
+
self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
|
|
354
354
|
|
|
355
355
|
def delete_pattern():
|
|
356
356
|
selection = tree.selection()
|
|
@@ -358,7 +358,7 @@ class RegexExtractorUI:
|
|
|
358
358
|
item_id = int(selection[0])
|
|
359
359
|
del pattern_library[item_id]
|
|
360
360
|
refresh_tree()
|
|
361
|
-
self.settings_manager.save_settings()
|
|
361
|
+
self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
|
|
362
362
|
|
|
363
363
|
def move_up():
|
|
364
364
|
selection = tree.selection()
|
|
@@ -371,7 +371,7 @@ class RegexExtractorUI:
|
|
|
371
371
|
refresh_tree()
|
|
372
372
|
tree.selection_set(str(item_id-1))
|
|
373
373
|
tree.focus(str(item_id-1))
|
|
374
|
-
self.settings_manager.save_settings()
|
|
374
|
+
self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
|
|
375
375
|
|
|
376
376
|
def move_down():
|
|
377
377
|
selection = tree.selection()
|
|
@@ -384,7 +384,7 @@ class RegexExtractorUI:
|
|
|
384
384
|
refresh_tree()
|
|
385
385
|
tree.selection_set(str(item_id+1))
|
|
386
386
|
tree.focus(str(item_id+1))
|
|
387
|
-
self.settings_manager.save_settings()
|
|
387
|
+
self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
|
|
388
388
|
|
|
389
389
|
ttk.Button(left_buttons, text="Add", command=add_pattern).pack(side=tk.LEFT, padx=(0,5))
|
|
390
390
|
ttk.Button(left_buttons, text="Delete", command=delete_pattern).pack(side=tk.LEFT, padx=5)
|
|
@@ -470,7 +470,7 @@ class RegexExtractorUI:
|
|
|
470
470
|
pattern["purpose"] = new_value
|
|
471
471
|
tree.set(item, column, new_value)
|
|
472
472
|
entry.destroy()
|
|
473
|
-
self.settings_manager.save_settings()
|
|
473
|
+
self.settings_manager.set_pattern_library(pattern_library) if hasattr(self.settings_manager, 'set_pattern_library') else self.settings_manager.save_settings()
|
|
474
474
|
|
|
475
475
|
def cancel_edit():
|
|
476
476
|
entry.destroy()
|