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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Diff Utilities Module
|
|
3
|
+
|
|
4
|
+
Reusable diff generation functions for Find & Replace preview,
|
|
5
|
+
MCP tools, and other components that need text comparison.
|
|
6
|
+
|
|
7
|
+
This module is UI-independent and can be used by both tkinter widgets
|
|
8
|
+
and CLI/MCP tools.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import difflib
|
|
12
|
+
import re
|
|
13
|
+
from typing import List, Tuple, Optional, NamedTuple
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DiffResult:
|
|
19
|
+
"""Result of a diff comparison operation."""
|
|
20
|
+
original_text: str
|
|
21
|
+
modified_text: str
|
|
22
|
+
unified_diff: str
|
|
23
|
+
replacements: int
|
|
24
|
+
lines_affected: int
|
|
25
|
+
similarity_score: float # 0-100
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class FindReplacePreview:
|
|
30
|
+
"""Preview of a find/replace operation before execution."""
|
|
31
|
+
original_text: str
|
|
32
|
+
modified_text: str
|
|
33
|
+
unified_diff: str
|
|
34
|
+
match_count: int
|
|
35
|
+
lines_affected: int
|
|
36
|
+
match_positions: List[Tuple[int, int]] # List of (start, end) character positions
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def generate_unified_diff(
|
|
40
|
+
original: str,
|
|
41
|
+
modified: str,
|
|
42
|
+
context_lines: int = 3,
|
|
43
|
+
original_label: str = "Original",
|
|
44
|
+
modified_label: str = "Modified"
|
|
45
|
+
) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Generate a unified diff between two texts.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
original: Original text
|
|
51
|
+
modified: Modified text
|
|
52
|
+
context_lines: Number of context lines around changes
|
|
53
|
+
original_label: Label for original text (shown as --- label)
|
|
54
|
+
modified_label: Label for modified text (shown as +++ label)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Unified diff as string
|
|
58
|
+
"""
|
|
59
|
+
original_lines = original.splitlines(keepends=True)
|
|
60
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
61
|
+
|
|
62
|
+
# Ensure last lines end with newline for proper diff
|
|
63
|
+
if original_lines and not original_lines[-1].endswith('\n'):
|
|
64
|
+
original_lines[-1] += '\n'
|
|
65
|
+
if modified_lines and not modified_lines[-1].endswith('\n'):
|
|
66
|
+
modified_lines[-1] += '\n'
|
|
67
|
+
|
|
68
|
+
diff = difflib.unified_diff(
|
|
69
|
+
original_lines,
|
|
70
|
+
modified_lines,
|
|
71
|
+
fromfile=original_label,
|
|
72
|
+
tofile=modified_label,
|
|
73
|
+
n=context_lines
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return ''.join(diff)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def generate_find_replace_preview(
|
|
80
|
+
text: str,
|
|
81
|
+
find_pattern: str,
|
|
82
|
+
replace_pattern: str,
|
|
83
|
+
use_regex: bool = True,
|
|
84
|
+
case_sensitive: bool = True,
|
|
85
|
+
context_lines: int = 2
|
|
86
|
+
) -> FindReplacePreview:
|
|
87
|
+
"""
|
|
88
|
+
Generate a preview of find/replace operation with unified diff.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
text: Input text to process
|
|
92
|
+
find_pattern: Pattern to find (regex or literal)
|
|
93
|
+
replace_pattern: Replacement string
|
|
94
|
+
use_regex: Whether find_pattern is a regex
|
|
95
|
+
case_sensitive: Whether to match case sensitively
|
|
96
|
+
context_lines: Lines of context in diff output
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
FindReplacePreview with diff and match information
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
re.error: If regex pattern is invalid
|
|
103
|
+
"""
|
|
104
|
+
if not find_pattern:
|
|
105
|
+
return FindReplacePreview(
|
|
106
|
+
original_text=text,
|
|
107
|
+
modified_text=text,
|
|
108
|
+
unified_diff="",
|
|
109
|
+
match_count=0,
|
|
110
|
+
lines_affected=0,
|
|
111
|
+
match_positions=[]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Compile the pattern
|
|
115
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
116
|
+
if use_regex:
|
|
117
|
+
pattern = re.compile(find_pattern, flags)
|
|
118
|
+
else:
|
|
119
|
+
pattern = re.compile(re.escape(find_pattern), flags)
|
|
120
|
+
|
|
121
|
+
# Find all matches and their positions
|
|
122
|
+
matches = list(pattern.finditer(text))
|
|
123
|
+
match_positions = [(m.start(), m.end()) for m in matches]
|
|
124
|
+
|
|
125
|
+
# Perform the replacement
|
|
126
|
+
modified_text = pattern.sub(replace_pattern, text)
|
|
127
|
+
|
|
128
|
+
# Calculate lines affected
|
|
129
|
+
original_lines = set()
|
|
130
|
+
pos = 0
|
|
131
|
+
line_num = 1
|
|
132
|
+
for char in text:
|
|
133
|
+
for match_start, match_end in match_positions:
|
|
134
|
+
if match_start <= pos < match_end:
|
|
135
|
+
original_lines.add(line_num)
|
|
136
|
+
if char == '\n':
|
|
137
|
+
line_num += 1
|
|
138
|
+
pos += 1
|
|
139
|
+
|
|
140
|
+
# Generate diff
|
|
141
|
+
match_info = f"({len(matches)} match{'es' if len(matches) != 1 else ''})"
|
|
142
|
+
unified_diff = generate_unified_diff(
|
|
143
|
+
text,
|
|
144
|
+
modified_text,
|
|
145
|
+
context_lines=context_lines,
|
|
146
|
+
original_label=f"Original {match_info}",
|
|
147
|
+
modified_label="Modified"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return FindReplacePreview(
|
|
151
|
+
original_text=text,
|
|
152
|
+
modified_text=modified_text,
|
|
153
|
+
unified_diff=unified_diff,
|
|
154
|
+
match_count=len(matches),
|
|
155
|
+
lines_affected=len(original_lines),
|
|
156
|
+
match_positions=match_positions
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def compute_similarity_score(original: str, modified: str) -> float:
|
|
161
|
+
"""
|
|
162
|
+
Compute similarity score between two texts (0-100).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
original: Original text
|
|
166
|
+
modified: Modified text
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Similarity percentage (0-100)
|
|
170
|
+
"""
|
|
171
|
+
if not original and not modified:
|
|
172
|
+
return 100.0
|
|
173
|
+
if not original or not modified:
|
|
174
|
+
return 0.0
|
|
175
|
+
|
|
176
|
+
matcher = difflib.SequenceMatcher(None, original, modified, autojunk=False)
|
|
177
|
+
return matcher.ratio() * 100
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generate_compact_diff(
|
|
181
|
+
original: str,
|
|
182
|
+
modified: str,
|
|
183
|
+
max_lines: int = 20
|
|
184
|
+
) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Generate a compact diff suitable for CLI/token-limited contexts.
|
|
187
|
+
Shows only changed lines without full context.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
original: Original text
|
|
191
|
+
modified: Modified text
|
|
192
|
+
max_lines: Maximum lines to show in diff
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Compact diff string
|
|
196
|
+
"""
|
|
197
|
+
original_lines = original.splitlines()
|
|
198
|
+
modified_lines = modified.splitlines()
|
|
199
|
+
|
|
200
|
+
matcher = difflib.SequenceMatcher(None, original_lines, modified_lines, autojunk=False)
|
|
201
|
+
|
|
202
|
+
output_lines = []
|
|
203
|
+
line_count = 0
|
|
204
|
+
|
|
205
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
206
|
+
if line_count >= max_lines:
|
|
207
|
+
output_lines.append(f"... ({max_lines}+ changes, truncated)")
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
if tag == 'equal':
|
|
211
|
+
continue
|
|
212
|
+
elif tag == 'delete':
|
|
213
|
+
for i in range(i1, i2):
|
|
214
|
+
if line_count >= max_lines:
|
|
215
|
+
break
|
|
216
|
+
output_lines.append(f"-{i1 + 1}: {original_lines[i]}")
|
|
217
|
+
line_count += 1
|
|
218
|
+
elif tag == 'insert':
|
|
219
|
+
for j in range(j1, j2):
|
|
220
|
+
if line_count >= max_lines:
|
|
221
|
+
break
|
|
222
|
+
output_lines.append(f"+{j1 + 1}: {modified_lines[j]}")
|
|
223
|
+
line_count += 1
|
|
224
|
+
elif tag == 'replace':
|
|
225
|
+
for i in range(i1, i2):
|
|
226
|
+
if line_count >= max_lines:
|
|
227
|
+
break
|
|
228
|
+
output_lines.append(f"-{i + 1}: {original_lines[i]}")
|
|
229
|
+
line_count += 1
|
|
230
|
+
for j in range(j1, j2):
|
|
231
|
+
if line_count >= max_lines:
|
|
232
|
+
break
|
|
233
|
+
output_lines.append(f"+{j + 1}: {modified_lines[j]}")
|
|
234
|
+
line_count += 1
|
|
235
|
+
|
|
236
|
+
if not output_lines:
|
|
237
|
+
return "No differences found."
|
|
238
|
+
|
|
239
|
+
return '\n'.join(output_lines)
|