pomera-ai-commander 1.1.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/LICENSE +21 -21
- package/README.md +105 -680
- package/bin/pomera-ai-commander.js +62 -62
- package/core/__init__.py +65 -65
- package/core/app_context.py +482 -482
- package/core/async_text_processor.py +421 -421
- package/core/backup_manager.py +655 -655
- package/core/backup_recovery_manager.py +1199 -1033
- package/core/content_hash_cache.py +508 -508
- package/core/context_menu.py +313 -313
- package/core/data_directory.py +549 -0
- package/core/data_validator.py +1066 -1066
- package/core/database_connection_manager.py +744 -744
- package/core/database_curl_settings_manager.py +608 -608
- package/core/database_promera_ai_settings_manager.py +446 -446
- package/core/database_schema.py +411 -411
- package/core/database_schema_manager.py +395 -395
- package/core/database_settings_manager.py +1507 -1507
- package/core/database_settings_manager_interface.py +456 -456
- package/core/dialog_manager.py +734 -734
- package/core/diff_utils.py +239 -0
- package/core/efficient_line_numbers.py +540 -510
- package/core/error_handler.py +746 -746
- package/core/error_service.py +431 -431
- package/core/event_consolidator.py +511 -511
- package/core/mcp/__init__.py +43 -43
- package/core/mcp/find_replace_diff.py +334 -0
- package/core/mcp/protocol.py +288 -288
- package/core/mcp/schema.py +251 -251
- package/core/mcp/server_stdio.py +299 -299
- package/core/mcp/tool_registry.py +2699 -2345
- package/core/memento.py +275 -0
- package/core/memory_efficient_text_widget.py +711 -711
- package/core/migration_manager.py +914 -914
- package/core/migration_test_suite.py +1085 -1085
- package/core/migration_validator.py +1143 -1143
- package/core/optimized_find_replace.py +714 -714
- package/core/optimized_pattern_engine.py +424 -424
- package/core/optimized_search_highlighter.py +552 -552
- package/core/performance_monitor.py +674 -674
- package/core/persistence_manager.py +712 -712
- package/core/progressive_stats_calculator.py +632 -632
- package/core/regex_pattern_cache.py +529 -529
- package/core/regex_pattern_library.py +350 -350
- package/core/search_operation_manager.py +434 -434
- package/core/settings_defaults_registry.py +1087 -1087
- package/core/settings_integrity_validator.py +1111 -1111
- package/core/settings_serializer.py +557 -557
- package/core/settings_validator.py +1823 -1823
- package/core/smart_stats_calculator.py +709 -709
- package/core/statistics_update_manager.py +619 -619
- package/core/stats_config_manager.py +858 -858
- package/core/streaming_text_handler.py +723 -723
- package/core/task_scheduler.py +596 -596
- package/core/update_pattern_library.py +168 -168
- package/core/visibility_monitor.py +596 -596
- package/core/widget_cache.py +498 -498
- package/mcp.json +51 -61
- package/migrate_data.py +127 -0
- package/package.json +64 -57
- package/pomera.py +7883 -7482
- package/pomera_mcp_server.py +183 -144
- package/requirements.txt +33 -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/__init__.py +4 -4
- package/tools/ai_tools.py +2891 -2891
- package/tools/ascii_art_generator.py +352 -352
- package/tools/base64_tools.py +183 -183
- package/tools/base_tool.py +511 -511
- package/tools/case_tool.py +308 -308
- package/tools/column_tools.py +395 -395
- package/tools/cron_tool.py +884 -884
- package/tools/curl_history.py +600 -600
- package/tools/curl_processor.py +1207 -1207
- package/tools/curl_settings.py +502 -502
- package/tools/curl_tool.py +5467 -5467
- package/tools/diff_viewer.py +1817 -1072
- package/tools/email_extraction_tool.py +248 -248
- package/tools/email_header_analyzer.py +425 -425
- package/tools/extraction_tools.py +250 -250
- package/tools/find_replace.py +2289 -1750
- package/tools/folder_file_reporter.py +1463 -1463
- package/tools/folder_file_reporter_adapter.py +480 -480
- package/tools/generator_tools.py +1216 -1216
- package/tools/hash_generator.py +255 -255
- package/tools/html_tool.py +656 -656
- package/tools/jsonxml_tool.py +729 -729
- package/tools/line_tools.py +419 -419
- package/tools/markdown_tools.py +561 -561
- package/tools/mcp_widget.py +1417 -1417
- package/tools/notes_widget.py +978 -973
- package/tools/number_base_converter.py +372 -372
- package/tools/regex_extractor.py +571 -571
- package/tools/slug_generator.py +310 -310
- package/tools/sorter_tools.py +458 -458
- package/tools/string_escape_tool.py +392 -392
- package/tools/text_statistics_tool.py +365 -365
- package/tools/text_wrapper.py +430 -430
- package/tools/timestamp_converter.py +421 -421
- package/tools/tool_loader.py +710 -710
- package/tools/translator_tools.py +522 -522
- package/tools/url_link_extractor.py +261 -261
- package/tools/url_parser.py +204 -204
- package/tools/whitespace_tools.py +355 -355
- package/tools/word_frequency_counter.py +146 -146
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/app_context.cpython-313.pyc +0 -0
- package/core/__pycache__/async_text_processor.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/backup_recovery_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/content_hash_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/context_menu.cpython-313.pyc +0 -0
- package/core/__pycache__/data_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/database_connection_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_curl_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_promera_ai_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema.cpython-313.pyc +0 -0
- package/core/__pycache__/database_schema_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/database_settings_manager_interface.cpython-313.pyc +0 -0
- package/core/__pycache__/dialog_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/efficient_line_numbers.cpython-313.pyc +0 -0
- package/core/__pycache__/error_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/error_service.cpython-313.pyc +0 -0
- package/core/__pycache__/event_consolidator.cpython-313.pyc +0 -0
- package/core/__pycache__/memory_efficient_text_widget.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_test_suite.cpython-313.pyc +0 -0
- package/core/__pycache__/migration_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_find_replace.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_pattern_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/optimized_search_highlighter.cpython-313.pyc +0 -0
- package/core/__pycache__/performance_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/persistence_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/progressive_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_cache.cpython-313.pyc +0 -0
- package/core/__pycache__/regex_pattern_library.cpython-313.pyc +0 -0
- package/core/__pycache__/search_operation_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_defaults_registry.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_integrity_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_serializer.cpython-313.pyc +0 -0
- package/core/__pycache__/settings_validator.cpython-313.pyc +0 -0
- package/core/__pycache__/smart_stats_calculator.cpython-313.pyc +0 -0
- package/core/__pycache__/statistics_update_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/stats_config_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/streaming_text_handler.cpython-313.pyc +0 -0
- package/core/__pycache__/task_scheduler.cpython-313.pyc +0 -0
- package/core/__pycache__/visibility_monitor.cpython-313.pyc +0 -0
- package/core/__pycache__/widget_cache.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/protocol.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/schema.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/server_stdio.cpython-313.pyc +0 -0
- package/core/mcp/__pycache__/tool_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/tools/__pycache__/ai_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/ascii_art_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/base64_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/base_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/case_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/column_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/cron_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_history.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_processor.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_settings.cpython-313.pyc +0 -0
- package/tools/__pycache__/curl_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/diff_viewer.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_extraction_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/email_header_analyzer.cpython-313.pyc +0 -0
- package/tools/__pycache__/extraction_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/find_replace.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter.cpython-313.pyc +0 -0
- package/tools/__pycache__/folder_file_reporter_adapter.cpython-313.pyc +0 -0
- package/tools/__pycache__/generator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/hash_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/html_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/huggingface_helper.cpython-313.pyc +0 -0
- package/tools/__pycache__/jsonxml_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/line_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/list_comparator.cpython-313.pyc +0 -0
- package/tools/__pycache__/markdown_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/notes_widget.cpython-313.pyc +0 -0
- package/tools/__pycache__/number_base_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/regex_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/slug_generator.cpython-313.pyc +0 -0
- package/tools/__pycache__/sorter_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/string_escape_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_statistics_tool.cpython-313.pyc +0 -0
- package/tools/__pycache__/text_wrapper.cpython-313.pyc +0 -0
- package/tools/__pycache__/timestamp_converter.cpython-313.pyc +0 -0
- package/tools/__pycache__/tool_loader.cpython-313.pyc +0 -0
- package/tools/__pycache__/translator_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_link_extractor.cpython-313.pyc +0 -0
- package/tools/__pycache__/url_parser.cpython-313.pyc +0 -0
- package/tools/__pycache__/whitespace_tools.cpython-313.pyc +0 -0
- package/tools/__pycache__/word_frequency_counter.cpython-313.pyc +0 -0
package/core/mcp/__init__.py
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP (Model Context Protocol) Module for Pomera AI Commander
|
|
3
|
-
|
|
4
|
-
This module provides bidirectional MCP functionality:
|
|
5
|
-
1. MCP Client - Connect to external MCP servers (filesystem, GitHub, etc.)
|
|
6
|
-
2. MCP Server - Expose Pomera's text tools to external AI assistants
|
|
7
|
-
|
|
8
|
-
Submodules:
|
|
9
|
-
- schema: Data classes for MCP types (Tool, Resource, Message)
|
|
10
|
-
- protocol: JSON-RPC 2.0 message handling
|
|
11
|
-
- tool_registry: Maps Pomera tools to MCP tool definitions
|
|
12
|
-
- server_stdio: stdio transport for MCP server
|
|
13
|
-
- resource_provider: Exposes tab contents as MCP resources
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from .schema import (
|
|
17
|
-
MCPMessage,
|
|
18
|
-
MCPTool,
|
|
19
|
-
MCPResource,
|
|
20
|
-
MCPError,
|
|
21
|
-
MCPErrorCode,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from .protocol import MCPProtocol
|
|
25
|
-
|
|
26
|
-
from .tool_registry import ToolRegistry, MCPToolAdapter
|
|
27
|
-
|
|
28
|
-
__all__ = [
|
|
29
|
-
# Schema
|
|
30
|
-
"MCPMessage",
|
|
31
|
-
"MCPTool",
|
|
32
|
-
"MCPResource",
|
|
33
|
-
"MCPError",
|
|
34
|
-
"MCPErrorCode",
|
|
35
|
-
# Protocol
|
|
36
|
-
"MCPProtocol",
|
|
37
|
-
# Tool Registry
|
|
38
|
-
"ToolRegistry",
|
|
39
|
-
"MCPToolAdapter",
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
__version__ = "0.1.0"
|
|
43
|
-
|
|
1
|
+
"""
|
|
2
|
+
MCP (Model Context Protocol) Module for Pomera AI Commander
|
|
3
|
+
|
|
4
|
+
This module provides bidirectional MCP functionality:
|
|
5
|
+
1. MCP Client - Connect to external MCP servers (filesystem, GitHub, etc.)
|
|
6
|
+
2. MCP Server - Expose Pomera's text tools to external AI assistants
|
|
7
|
+
|
|
8
|
+
Submodules:
|
|
9
|
+
- schema: Data classes for MCP types (Tool, Resource, Message)
|
|
10
|
+
- protocol: JSON-RPC 2.0 message handling
|
|
11
|
+
- tool_registry: Maps Pomera tools to MCP tool definitions
|
|
12
|
+
- server_stdio: stdio transport for MCP server
|
|
13
|
+
- resource_provider: Exposes tab contents as MCP resources
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .schema import (
|
|
17
|
+
MCPMessage,
|
|
18
|
+
MCPTool,
|
|
19
|
+
MCPResource,
|
|
20
|
+
MCPError,
|
|
21
|
+
MCPErrorCode,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .protocol import MCPProtocol
|
|
25
|
+
|
|
26
|
+
from .tool_registry import ToolRegistry, MCPToolAdapter
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Schema
|
|
30
|
+
"MCPMessage",
|
|
31
|
+
"MCPTool",
|
|
32
|
+
"MCPResource",
|
|
33
|
+
"MCPError",
|
|
34
|
+
"MCPErrorCode",
|
|
35
|
+
# Protocol
|
|
36
|
+
"MCPProtocol",
|
|
37
|
+
# Tool Registry
|
|
38
|
+
"ToolRegistry",
|
|
39
|
+
"MCPToolAdapter",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
__version__ = "0.1.0"
|
|
43
|
+
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Find & Replace Diff MCP Tool
|
|
3
|
+
|
|
4
|
+
Provides regex find/replace with diff preview and automatic backup to Notes.
|
|
5
|
+
Designed for AI agent workflows requiring verification and rollback capability.
|
|
6
|
+
|
|
7
|
+
Operations:
|
|
8
|
+
- validate: Check regex syntax before use
|
|
9
|
+
- preview: Show unified diff of proposed changes
|
|
10
|
+
- execute: Perform replacement with automatic backup to Notes
|
|
11
|
+
- recall: Retrieve previous operation state for rollback
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Dict, Any, Optional, List, Tuple
|
|
18
|
+
from dataclasses import dataclass, asdict
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
# Import diff utilities
|
|
22
|
+
try:
|
|
23
|
+
from core.diff_utils import generate_find_replace_preview, generate_compact_diff, FindReplacePreview
|
|
24
|
+
DIFF_UTILS_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
DIFF_UTILS_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FindReplaceOperation:
|
|
33
|
+
"""Represents a find/replace operation for storage in Notes."""
|
|
34
|
+
find_pattern: str
|
|
35
|
+
replace_pattern: str
|
|
36
|
+
flags: List[str]
|
|
37
|
+
original_text: str
|
|
38
|
+
modified_text: str
|
|
39
|
+
match_count: int
|
|
40
|
+
timestamp: str
|
|
41
|
+
|
|
42
|
+
def to_json(self) -> str:
|
|
43
|
+
return json.dumps(asdict(self), ensure_ascii=False)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_json(cls, json_str: str) -> 'FindReplaceOperation':
|
|
47
|
+
data = json.loads(json_str)
|
|
48
|
+
return cls(**data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_regex(pattern: str, flags: List[str] = None) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Validate a regex pattern.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
pattern: Regex pattern string
|
|
57
|
+
flags: Optional list of flag characters ('i', 'm', 's', 'x')
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dict with validation result
|
|
61
|
+
"""
|
|
62
|
+
if not pattern:
|
|
63
|
+
return {"valid": True, "pattern": "", "groups": 0, "flags_applied": []}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Convert flag characters to re flags
|
|
67
|
+
re_flags = 0
|
|
68
|
+
flags_applied = []
|
|
69
|
+
if flags:
|
|
70
|
+
flag_map = {
|
|
71
|
+
'i': (re.IGNORECASE, 'IGNORECASE'),
|
|
72
|
+
'm': (re.MULTILINE, 'MULTILINE'),
|
|
73
|
+
's': (re.DOTALL, 'DOTALL'),
|
|
74
|
+
'x': (re.VERBOSE, 'VERBOSE')
|
|
75
|
+
}
|
|
76
|
+
for f in flags:
|
|
77
|
+
if f.lower() in flag_map:
|
|
78
|
+
re_flags |= flag_map[f.lower()][0]
|
|
79
|
+
flags_applied.append(flag_map[f.lower()][1])
|
|
80
|
+
|
|
81
|
+
compiled = re.compile(pattern, re_flags)
|
|
82
|
+
return {
|
|
83
|
+
"valid": True,
|
|
84
|
+
"pattern": pattern,
|
|
85
|
+
"groups": compiled.groups,
|
|
86
|
+
"flags_applied": flags_applied
|
|
87
|
+
}
|
|
88
|
+
except re.error as e:
|
|
89
|
+
suggestion = _get_regex_suggestion(str(e))
|
|
90
|
+
return {
|
|
91
|
+
"valid": False,
|
|
92
|
+
"pattern": pattern,
|
|
93
|
+
"error": str(e),
|
|
94
|
+
"suggestion": suggestion
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def preview_replace(
|
|
99
|
+
text: str,
|
|
100
|
+
find_pattern: str,
|
|
101
|
+
replace_pattern: str,
|
|
102
|
+
flags: List[str] = None,
|
|
103
|
+
context_lines: int = 2,
|
|
104
|
+
max_diff_lines: int = 50
|
|
105
|
+
) -> Dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Generate a preview of find/replace operation with compact diff.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
text: Input text to process
|
|
111
|
+
find_pattern: Regex pattern to find
|
|
112
|
+
replace_pattern: Replacement string
|
|
113
|
+
flags: Optional regex flags
|
|
114
|
+
context_lines: Lines of context in diff
|
|
115
|
+
max_diff_lines: Maximum diff lines to return (token efficiency)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dict with preview information
|
|
119
|
+
"""
|
|
120
|
+
# Validate first
|
|
121
|
+
validation = validate_regex(find_pattern, flags)
|
|
122
|
+
if not validation["valid"]:
|
|
123
|
+
return {"success": False, "error": validation["error"], "suggestion": validation.get("suggestion", "")}
|
|
124
|
+
|
|
125
|
+
# Build regex flags
|
|
126
|
+
re_flags = 0
|
|
127
|
+
if flags:
|
|
128
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL, 'x': re.VERBOSE}
|
|
129
|
+
for f in flags:
|
|
130
|
+
if f.lower() in flag_map:
|
|
131
|
+
re_flags |= flag_map[f.lower()]
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
pattern = re.compile(find_pattern, re_flags)
|
|
135
|
+
matches = list(pattern.finditer(text))
|
|
136
|
+
|
|
137
|
+
if not matches:
|
|
138
|
+
return {
|
|
139
|
+
"success": True,
|
|
140
|
+
"match_count": 0,
|
|
141
|
+
"diff": "No matches found.",
|
|
142
|
+
"lines_affected": 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Perform replacement
|
|
146
|
+
modified_text = pattern.sub(replace_pattern, text)
|
|
147
|
+
|
|
148
|
+
# Generate compact diff (token-efficient)
|
|
149
|
+
diff = generate_compact_diff(text, modified_text, max_lines=max_diff_lines) if DIFF_UTILS_AVAILABLE else _basic_diff(text, modified_text)
|
|
150
|
+
|
|
151
|
+
# Count affected lines
|
|
152
|
+
lines_affected = len(set(text[:m.start()].count('\n') + 1 for m in matches))
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"success": True,
|
|
156
|
+
"match_count": len(matches),
|
|
157
|
+
"lines_affected": lines_affected,
|
|
158
|
+
"diff": diff
|
|
159
|
+
}
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return {"success": False, "error": str(e)}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def execute_replace(
|
|
165
|
+
text: str,
|
|
166
|
+
find_pattern: str,
|
|
167
|
+
replace_pattern: str,
|
|
168
|
+
flags: List[str] = None,
|
|
169
|
+
save_to_notes: bool = True,
|
|
170
|
+
notes_handler = None
|
|
171
|
+
) -> Dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Execute find/replace with optional backup to Notes.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
text: Input text to process
|
|
177
|
+
find_pattern: Regex pattern to find
|
|
178
|
+
replace_pattern: Replacement string
|
|
179
|
+
flags: Optional regex flags
|
|
180
|
+
save_to_notes: Whether to save operation to Notes for rollback
|
|
181
|
+
notes_handler: Function to save to notes (called as notes_handler(title, input_content, output_content))
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Dict with execution result including note_id if saved
|
|
185
|
+
"""
|
|
186
|
+
# Validate first
|
|
187
|
+
validation = validate_regex(find_pattern, flags)
|
|
188
|
+
if not validation["valid"]:
|
|
189
|
+
return {"success": False, "error": validation["error"]}
|
|
190
|
+
|
|
191
|
+
# Build regex flags
|
|
192
|
+
re_flags = 0
|
|
193
|
+
if flags:
|
|
194
|
+
flag_map = {'i': re.IGNORECASE, 'm': re.MULTILINE, 's': re.DOTALL, 'x': re.VERBOSE}
|
|
195
|
+
for f in flags:
|
|
196
|
+
if f.lower() in flag_map:
|
|
197
|
+
re_flags |= flag_map[f.lower()]
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
pattern = re.compile(find_pattern, re_flags)
|
|
201
|
+
matches = list(pattern.finditer(text))
|
|
202
|
+
|
|
203
|
+
if not matches:
|
|
204
|
+
return {
|
|
205
|
+
"success": True,
|
|
206
|
+
"replacements": 0,
|
|
207
|
+
"modified_text": text,
|
|
208
|
+
"note_id": None
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Perform replacement
|
|
212
|
+
modified_text = pattern.sub(replace_pattern, text)
|
|
213
|
+
|
|
214
|
+
# Count affected lines
|
|
215
|
+
lines_affected = len(set(text[:m.start()].count('\n') + 1 for m in matches))
|
|
216
|
+
|
|
217
|
+
result = {
|
|
218
|
+
"success": True,
|
|
219
|
+
"replacements": len(matches),
|
|
220
|
+
"lines_affected": lines_affected,
|
|
221
|
+
"modified_text": modified_text,
|
|
222
|
+
"note_id": None
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Save to notes if requested
|
|
226
|
+
if save_to_notes and notes_handler:
|
|
227
|
+
try:
|
|
228
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
229
|
+
title = f"FindReplace/{timestamp}"
|
|
230
|
+
|
|
231
|
+
# Create operation record
|
|
232
|
+
operation = FindReplaceOperation(
|
|
233
|
+
find_pattern=find_pattern,
|
|
234
|
+
replace_pattern=replace_pattern,
|
|
235
|
+
flags=flags or [],
|
|
236
|
+
original_text=text,
|
|
237
|
+
modified_text=modified_text,
|
|
238
|
+
match_count=len(matches),
|
|
239
|
+
timestamp=timestamp
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
note_id = notes_handler(
|
|
243
|
+
title=title,
|
|
244
|
+
input_content=text,
|
|
245
|
+
output_content=operation.to_json()
|
|
246
|
+
)
|
|
247
|
+
result["note_id"] = note_id
|
|
248
|
+
result["note_title"] = title
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.warning(f"Failed to save operation to notes: {e}")
|
|
251
|
+
result["note_error"] = str(e)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
except Exception as e:
|
|
255
|
+
return {"success": False, "error": str(e)}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def recall_operation(note_id: int, notes_getter = None) -> Dict[str, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Recall a previous find/replace operation from Notes.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
note_id: ID of the note to recall
|
|
264
|
+
notes_getter: Function to get note by ID (returns dict with 'output_content')
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Dict with recalled operation details
|
|
268
|
+
"""
|
|
269
|
+
if not notes_getter:
|
|
270
|
+
return {"success": False, "error": "Notes getter not available"}
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
note = notes_getter(note_id)
|
|
274
|
+
if not note:
|
|
275
|
+
return {"success": False, "error": f"Note {note_id} not found"}
|
|
276
|
+
|
|
277
|
+
# Parse the operation from output_content
|
|
278
|
+
operation = FindReplaceOperation.from_json(note.get('output_content', '{}'))
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"success": True,
|
|
282
|
+
"note_id": note_id,
|
|
283
|
+
"title": note.get('title', ''),
|
|
284
|
+
"find_pattern": operation.find_pattern,
|
|
285
|
+
"replace_pattern": operation.replace_pattern,
|
|
286
|
+
"flags": operation.flags,
|
|
287
|
+
"original_text": operation.original_text,
|
|
288
|
+
"modified_text": operation.modified_text,
|
|
289
|
+
"match_count": operation.match_count,
|
|
290
|
+
"timestamp": operation.timestamp
|
|
291
|
+
}
|
|
292
|
+
except Exception as e:
|
|
293
|
+
return {"success": False, "error": str(e)}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _get_regex_suggestion(error_msg: str) -> str:
|
|
297
|
+
"""Get helpful suggestion for common regex errors."""
|
|
298
|
+
suggestions = {
|
|
299
|
+
"unterminated": "Check for unmatched parentheses, brackets, or quotes",
|
|
300
|
+
"unbalanced": "Ensure all opening ( [ { have matching closing ) ] }",
|
|
301
|
+
"nothing to repeat": "Quantifiers like * + ? need something before them",
|
|
302
|
+
"bad escape": "Use double backslash \\\\ or raw string r'' for special chars",
|
|
303
|
+
"look-behind": "Python look-behind requires fixed-width pattern",
|
|
304
|
+
"bad character range": "Check character ranges like [a-z], ensure start < end"
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
error_lower = error_msg.lower()
|
|
308
|
+
for key, suggestion in suggestions.items():
|
|
309
|
+
if key in error_lower:
|
|
310
|
+
return suggestion
|
|
311
|
+
|
|
312
|
+
return "Check regex syntax - see Python re module documentation"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _basic_diff(original: str, modified: str) -> str:
|
|
316
|
+
"""Basic diff when diff_utils not available."""
|
|
317
|
+
orig_lines = original.splitlines()
|
|
318
|
+
mod_lines = modified.splitlines()
|
|
319
|
+
|
|
320
|
+
output = []
|
|
321
|
+
for i, (o, m) in enumerate(zip(orig_lines, mod_lines)):
|
|
322
|
+
if o != m:
|
|
323
|
+
output.append(f"-{i+1}: {o}")
|
|
324
|
+
output.append(f"+{i+1}: {m}")
|
|
325
|
+
|
|
326
|
+
# Handle length differences
|
|
327
|
+
if len(mod_lines) > len(orig_lines):
|
|
328
|
+
for i in range(len(orig_lines), len(mod_lines)):
|
|
329
|
+
output.append(f"+{i+1}: {mod_lines[i]}")
|
|
330
|
+
elif len(orig_lines) > len(mod_lines):
|
|
331
|
+
for i in range(len(mod_lines), len(orig_lines)):
|
|
332
|
+
output.append(f"-{i+1}: {orig_lines[i]}")
|
|
333
|
+
|
|
334
|
+
return '\n'.join(output) if output else "No differences"
|