kollabor 0.4.9__py3-none-any.whl

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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
@@ -0,0 +1,1401 @@
1
+ """File operations executor for LLM-driven file manipulation.
2
+
3
+ Provides safe file operations with automatic backups, validation, and
4
+ comprehensive error handling. Implements 11 file operation types:
5
+ - edit: Find/replace in files (replaces ALL occurrences)
6
+ - create: Create new files
7
+ - create_overwrite: Create/overwrite files
8
+ - delete: Delete files with safety checks
9
+ - move: Move/rename files
10
+ - copy: Copy files
11
+ - copy_overwrite: Copy with overwrite
12
+ - append: Append to files
13
+ - insert_after: Insert content after pattern (exact match required)
14
+ - insert_before: Insert content before pattern (exact match required)
15
+ - mkdir: Create directories
16
+ - rmdir: Remove empty directories
17
+ - read: Read file content
18
+ """
19
+
20
+ import ast
21
+ import logging
22
+ import os
23
+ import shutil
24
+ from pathlib import Path
25
+ from typing import Any, Dict, List, Optional, Tuple
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class FileOperationsExecutor:
31
+ """Execute file operations with comprehensive safety features.
32
+
33
+ Features:
34
+ - Automatic backups before destructive operations
35
+ - Protected path checking
36
+ - Path traversal prevention
37
+ - Binary file detection
38
+ - Optional Python syntax validation
39
+ - File size limits
40
+ - Multi-occurrence handling (edit vs insert operations)
41
+ """
42
+
43
+ def __init__(self, config=None):
44
+ """Initialize file operations executor.
45
+
46
+ Args:
47
+ config: Configuration manager (optional)
48
+ """
49
+ self.config = config
50
+
51
+ # Default configuration values
52
+ self.enabled = self._get_config("file_operations.enabled", True)
53
+ self.automatic_backups = self._get_config("file_operations.automatic_backups", True)
54
+ self.validate_python_syntax = self._get_config("file_operations.validate_python_syntax", True)
55
+ self.rollback_on_syntax_error = self._get_config("file_operations.rollback_on_syntax_error", True)
56
+ self.max_edit_size_mb = self._get_config("file_operations.max_edit_size_mb", 10)
57
+ self.max_create_size_mb = self._get_config("file_operations.max_create_size_mb", 5)
58
+ self.max_read_size_mb = self._get_config("file_operations.max_read_size_mb", 10)
59
+ self.create_parent_directories = self._get_config("file_operations.create_parent_directories", True)
60
+ self.allow_binary_operations = self._get_config("file_operations.allow_binary_operations", False)
61
+
62
+ # Protected paths (cannot delete/modify)
63
+ self.protected_files = self._get_config("file_operations.protected_files", [
64
+ "core/application.py",
65
+ "main.py"
66
+ ])
67
+
68
+ self.protected_patterns = self._get_config("file_operations.protected_patterns", [
69
+ ".git/**",
70
+ "venv/**",
71
+ ".venv/**",
72
+ "node_modules/**"
73
+ ])
74
+
75
+ logger.info(f"File operations executor initialized (enabled={self.enabled})")
76
+
77
+ def _get_config(self, key: str, default: Any) -> Any:
78
+ """Get configuration value with fallback.
79
+
80
+ Args:
81
+ key: Config key in dot notation
82
+ default: Default value if not found
83
+
84
+ Returns:
85
+ Configuration value or default
86
+ """
87
+ if self.config:
88
+ return self.config.get(key, default)
89
+ return default
90
+
91
+ def validate_file_path(self, filepath: str) -> Tuple[bool, str]:
92
+ """Validate file path for security and correctness.
93
+
94
+ Args:
95
+ filepath: File path to validate
96
+
97
+ Returns:
98
+ (is_valid, error_message)
99
+ """
100
+ if not filepath:
101
+ return False, "Empty file path"
102
+
103
+ # Security: No path traversal
104
+ if ".." in filepath:
105
+ return False, f"Path traversal detected: {filepath}"
106
+
107
+ # Security: No absolute paths (relative paths only)
108
+ if filepath.startswith("/") or (len(filepath) > 1 and filepath[1] == ":"):
109
+ return False, f"Absolute paths not allowed: {filepath}"
110
+
111
+ # Practical: Path length limit
112
+ if len(filepath) > 255:
113
+ return False, f"Path too long: {len(filepath)} chars (max 255)"
114
+
115
+ # Security: No null bytes
116
+ if "\x00" in filepath:
117
+ return False, "Null byte in file path"
118
+
119
+ return True, ""
120
+
121
+ def is_protected_path(self, filepath: str) -> bool:
122
+ """Check if file path is protected from deletion/modification.
123
+
124
+ Args:
125
+ filepath: File path to check
126
+
127
+ Returns:
128
+ True if path is protected
129
+ """
130
+ # Check exact matches
131
+ if filepath in self.protected_files:
132
+ return True
133
+
134
+ # Check pattern matches
135
+ path_obj = Path(filepath)
136
+ for pattern in self.protected_patterns:
137
+ # Simple wildcard matching
138
+ if pattern.endswith("/**"):
139
+ prefix = pattern[:-3]
140
+ if str(path_obj).startswith(prefix):
141
+ return True
142
+ elif pattern.endswith("/*"):
143
+ prefix = pattern[:-2]
144
+ if str(path_obj.parent).startswith(prefix):
145
+ return True
146
+
147
+ return False
148
+
149
+ def is_text_file(self, filepath: str) -> bool:
150
+ """Check if file is text (not binary).
151
+
152
+ Args:
153
+ filepath: File path to check
154
+
155
+ Returns:
156
+ True if file is text
157
+ """
158
+ try:
159
+ with open(filepath, 'r', encoding='utf-8') as f:
160
+ f.read(1024) # Try reading first 1KB as text
161
+ return True
162
+ except (UnicodeDecodeError, FileNotFoundError):
163
+ return False
164
+
165
+ def check_file_size(self, filepath: str, max_size_mb: float) -> Tuple[bool, str]:
166
+ """Check if file size is within limits.
167
+
168
+ Args:
169
+ filepath: File path to check
170
+ max_size_mb: Maximum size in MB
171
+
172
+ Returns:
173
+ (is_valid, error_message)
174
+ """
175
+ try:
176
+ size_bytes = os.path.getsize(filepath)
177
+ size_mb = size_bytes / (1024 * 1024)
178
+
179
+ if size_mb > max_size_mb:
180
+ return False, f"File too large: {size_mb:.1f}MB (max {max_size_mb}MB)"
181
+
182
+ return True, ""
183
+ except FileNotFoundError:
184
+ return True, "" # File doesn't exist yet, allow operation
185
+
186
+ def create_backup(self, filepath: str, suffix: str = ".bak") -> Optional[str]:
187
+ """Create backup of file before modification.
188
+
189
+ Args:
190
+ filepath: File to backup
191
+ suffix: Backup file suffix
192
+
193
+ Returns:
194
+ Backup file path or None if failed
195
+ """
196
+ if not self.automatic_backups:
197
+ return None
198
+
199
+ if not os.path.exists(filepath):
200
+ return None
201
+
202
+ backup_path = f"{filepath}{suffix}"
203
+
204
+ try:
205
+ shutil.copy2(filepath, backup_path)
206
+ logger.debug(f"Created backup: {backup_path}")
207
+ return backup_path
208
+ except Exception as e:
209
+ logger.error(f"Failed to create backup: {e}")
210
+ return None
211
+
212
+ def validate_python_syntax_file(self, filepath: str) -> Tuple[bool, str]:
213
+ """Validate Python file syntax.
214
+
215
+ Args:
216
+ filepath: Python file to validate
217
+
218
+ Returns:
219
+ (is_valid, error_message)
220
+ """
221
+ if not self.validate_python_syntax:
222
+ return True, ""
223
+
224
+ if not filepath.endswith('.py'):
225
+ return True, ""
226
+
227
+ try:
228
+ with open(filepath, 'r') as f:
229
+ content = f.read()
230
+ ast.parse(content)
231
+ return True, ""
232
+ except SyntaxError as e:
233
+ return False, f"Syntax error at line {e.lineno}: {e.msg}"
234
+ except Exception as e:
235
+ return False, f"Validation error: {str(e)}"
236
+
237
+ def find_pattern_occurrences(self, content: str, pattern: str) -> List[int]:
238
+ """Find all line numbers where pattern occurs.
239
+
240
+ Args:
241
+ content: File content
242
+ pattern: Pattern to search for
243
+
244
+ Returns:
245
+ List of line numbers (1-indexed) where pattern appears
246
+ """
247
+ lines = content.split('\n')
248
+ occurrences = []
249
+
250
+ # For multi-line patterns
251
+ pattern_lines = pattern.split('\n')
252
+ pattern_len = len(pattern_lines)
253
+
254
+ for i in range(len(lines) - pattern_len + 1):
255
+ # Check if pattern matches starting at line i
256
+ matches = True
257
+ for j in range(pattern_len):
258
+ if pattern_lines[j] != lines[i + j]:
259
+ matches = False
260
+ break
261
+
262
+ if matches:
263
+ occurrences.append(i + 1) # 1-indexed
264
+
265
+ # Also try simple substring search if no multi-line matches
266
+ if not occurrences and pattern in content:
267
+ for i, line in enumerate(lines, 1):
268
+ if pattern in line:
269
+ occurrences.append(i)
270
+
271
+ return occurrences
272
+
273
+ def execute_operation(self, operation: Dict[str, Any]) -> Dict[str, Any]:
274
+ """Execute a file operation.
275
+
276
+ Args:
277
+ operation: Operation dictionary from parser
278
+
279
+ Returns:
280
+ Result dictionary with success, output, error
281
+ """
282
+ if not self.enabled:
283
+ return {
284
+ "success": False,
285
+ "error": "File operations are disabled in configuration"
286
+ }
287
+
288
+ op_type = operation.get("type", "unknown")
289
+ op_id = operation.get("id", "unknown")
290
+
291
+ logger.info(f"Executing file operation: {op_type} ({op_id})")
292
+
293
+ # Malformed operations - provide helpful error message
294
+ if op_type == "malformed_file_op":
295
+ op_name = operation.get('operation', 'unknown')
296
+ error_msg = operation.get('error', 'Unknown error')
297
+ expected = operation.get('expected_format', '')
298
+ preview = operation.get('content_preview', '')
299
+
300
+ error_lines = [
301
+ f"Malformed <{op_name}> operation: {error_msg}",
302
+ "",
303
+ "Expected format:",
304
+ expected,
305
+ ]
306
+ if preview:
307
+ error_lines.extend([
308
+ "",
309
+ "Received:",
310
+ preview[:200] + "..." if len(preview) > 200 else preview
311
+ ])
312
+
313
+ return {
314
+ "success": False,
315
+ "error": "\n".join(error_lines)
316
+ }
317
+
318
+ # Route to specific operation handler
319
+ handlers = {
320
+ "file_edit": self._execute_edit,
321
+ "file_create": self._execute_create,
322
+ "file_create_overwrite": self._execute_create_overwrite,
323
+ "file_delete": self._execute_delete,
324
+ "file_move": self._execute_move,
325
+ "file_copy": self._execute_copy,
326
+ "file_copy_overwrite": self._execute_copy_overwrite,
327
+ "file_append": self._execute_append,
328
+ "file_insert_after": self._execute_insert_after,
329
+ "file_insert_before": self._execute_insert_before,
330
+ "file_mkdir": self._execute_mkdir,
331
+ "file_rmdir": self._execute_rmdir,
332
+ "file_read": self._execute_read,
333
+ "file_grep": self._execute_grep
334
+ }
335
+
336
+ handler = handlers.get(op_type)
337
+
338
+ if not handler:
339
+ return {
340
+ "success": False,
341
+ "error": f"Unknown operation type: {op_type}"
342
+ }
343
+
344
+ try:
345
+ return handler(operation)
346
+ except Exception as e:
347
+ import traceback
348
+ error_trace = traceback.format_exc()
349
+ logger.error(f"Operation {op_type} failed: {error_trace}")
350
+ return {
351
+ "success": False,
352
+ "error": f"Operation failed: {str(e)}"
353
+ }
354
+
355
+ def _execute_edit(self, operation: Dict[str, Any]) -> Dict[str, Any]:
356
+ """Execute file edit operation (find/replace).
357
+
358
+ Behavior: Replaces ALL occurrences, reports count.
359
+
360
+ Args:
361
+ operation: Operation data
362
+
363
+ Returns:
364
+ Result dictionary
365
+ """
366
+ filepath = operation.get("file")
367
+ find_content = operation.get("find")
368
+ replace_content = operation.get("replace")
369
+
370
+ # Validation
371
+ if not filepath or not find_content or replace_content is None:
372
+ return {
373
+ "success": False,
374
+ "error": "Missing required fields: file, find, replace"
375
+ }
376
+
377
+ is_valid, error = self.validate_file_path(filepath)
378
+ if not is_valid:
379
+ return {"success": False, "error": error}
380
+
381
+ if not os.path.exists(filepath):
382
+ return {
383
+ "success": False,
384
+ "error": f"File not found: {filepath}"
385
+ }
386
+
387
+ # Check file size
388
+ is_valid, error = self.check_file_size(filepath, self.max_edit_size_mb)
389
+ if not is_valid:
390
+ return {"success": False, "error": error}
391
+
392
+ # Check if text file
393
+ if not self.is_text_file(filepath):
394
+ return {
395
+ "success": False,
396
+ "error": f"Cannot edit binary file: {filepath}"
397
+ }
398
+
399
+ # Read file
400
+ try:
401
+ with open(filepath, 'r', encoding='utf-8') as f:
402
+ content = f.read()
403
+ except Exception as e:
404
+ return {
405
+ "success": False,
406
+ "error": f"Failed to read file: {str(e)}"
407
+ }
408
+
409
+ # Check pattern exists
410
+ count = content.count(find_content)
411
+ if count == 0:
412
+ return {
413
+ "success": False,
414
+ "error": f"Pattern not found in {filepath}"
415
+ }
416
+
417
+ # Find line numbers for reporting
418
+ line_numbers = self.find_pattern_occurrences(content, find_content)
419
+
420
+ # Create backup
421
+ backup_path = self.create_backup(filepath)
422
+
423
+ # Perform replacement (REPLACE ALL)
424
+ new_content = content.replace(find_content, replace_content)
425
+
426
+ # Write back
427
+ try:
428
+ with open(filepath, 'w', encoding='utf-8') as f:
429
+ f.write(new_content)
430
+ except Exception as e:
431
+ # Restore from backup if write fails
432
+ if backup_path and os.path.exists(backup_path):
433
+ shutil.copy2(backup_path, filepath)
434
+ return {
435
+ "success": False,
436
+ "error": f"Failed to write file: {str(e)}"
437
+ }
438
+
439
+ # Optional: Validate Python syntax
440
+ if filepath.endswith('.py') and self.validate_python_syntax:
441
+ is_valid, error = self.validate_python_syntax_file(filepath)
442
+ if not is_valid and self.rollback_on_syntax_error:
443
+ # Rollback
444
+ if backup_path and os.path.exists(backup_path):
445
+ shutil.copy2(backup_path, filepath)
446
+ return {
447
+ "success": False,
448
+ "error": f"Syntax validation failed: {error}. Edit rolled back."
449
+ }
450
+
451
+ # Build success message with diff info
452
+ if count == 1:
453
+ output = f"✓ Replaced 1 occurrence in {filepath}"
454
+ else:
455
+ lines_str = ", ".join(str(ln) for ln in line_numbers[:10])
456
+ if len(line_numbers) > 10:
457
+ lines_str += f" (+{len(line_numbers) - 10} more)"
458
+ output = f"✓ Replaced {count} occurrences in {filepath}\nLocations: lines {lines_str}"
459
+
460
+ if backup_path:
461
+ output += f"\nBackup: {backup_path}"
462
+
463
+ return {
464
+ "success": True,
465
+ "output": output,
466
+ "diff_info": {
467
+ "find": find_content,
468
+ "replace": replace_content,
469
+ "count": count,
470
+ "lines": line_numbers[:5] # First 5 line numbers for context
471
+ }
472
+ }
473
+
474
+ def _execute_create(self, operation: Dict[str, Any]) -> Dict[str, Any]:
475
+ """Execute file create operation.
476
+
477
+ Fails if file already exists.
478
+
479
+ Args:
480
+ operation: Operation data
481
+
482
+ Returns:
483
+ Result dictionary
484
+ """
485
+ filepath = operation.get("file")
486
+ content = operation.get("content", "")
487
+
488
+ # Validation
489
+ if not filepath:
490
+ return {"success": False, "error": "Missing file path"}
491
+
492
+ is_valid, error = self.validate_file_path(filepath)
493
+ if not is_valid:
494
+ return {"success": False, "error": error}
495
+
496
+ if os.path.exists(filepath):
497
+ return {
498
+ "success": False,
499
+ "error": f"File already exists: {filepath}. Use <edit> to modify or <create_overwrite> to replace."
500
+ }
501
+
502
+ # Check content size
503
+ size_mb = len(content.encode('utf-8')) / (1024 * 1024)
504
+ if size_mb > self.max_create_size_mb:
505
+ return {
506
+ "success": False,
507
+ "error": f"Content too large: {size_mb:.1f}MB (max {self.max_create_size_mb}MB)"
508
+ }
509
+
510
+ # Create parent directories if needed
511
+ parent_dir = os.path.dirname(filepath)
512
+ if parent_dir and not os.path.exists(parent_dir):
513
+ if self.create_parent_directories:
514
+ try:
515
+ os.makedirs(parent_dir, exist_ok=True)
516
+ logger.debug(f"Created parent directories: {parent_dir}")
517
+ except Exception as e:
518
+ return {
519
+ "success": False,
520
+ "error": f"Failed to create parent directories: {str(e)}"
521
+ }
522
+ else:
523
+ return {
524
+ "success": False,
525
+ "error": f"Parent directory does not exist: {parent_dir}"
526
+ }
527
+
528
+ # Write file
529
+ try:
530
+ with open(filepath, 'w', encoding='utf-8') as f:
531
+ f.write(content)
532
+
533
+ # Set permissions (644 = rw-r--r--)
534
+ os.chmod(filepath, 0o644)
535
+
536
+ size_bytes = len(content.encode('utf-8'))
537
+ return {
538
+ "success": True,
539
+ "output": f"✓ Created {filepath} ({size_bytes} bytes)"
540
+ }
541
+ except Exception as e:
542
+ return {
543
+ "success": False,
544
+ "error": f"Failed to create file: {str(e)}"
545
+ }
546
+
547
+ def _execute_create_overwrite(self, operation: Dict[str, Any]) -> Dict[str, Any]:
548
+ """Execute file create with overwrite.
549
+
550
+ Creates backup if file exists.
551
+
552
+ Args:
553
+ operation: Operation data
554
+
555
+ Returns:
556
+ Result dictionary
557
+ """
558
+ filepath = operation.get("file")
559
+ content = operation.get("content", "")
560
+
561
+ # Validation
562
+ if not filepath:
563
+ return {"success": False, "error": "Missing file path"}
564
+
565
+ is_valid, error = self.validate_file_path(filepath)
566
+ if not is_valid:
567
+ return {"success": False, "error": error}
568
+
569
+ # Create backup if file exists
570
+ backup_path = None
571
+ if os.path.exists(filepath):
572
+ backup_path = self.create_backup(filepath)
573
+
574
+ # Check content size
575
+ size_mb = len(content.encode('utf-8')) / (1024 * 1024)
576
+ if size_mb > self.max_create_size_mb:
577
+ return {
578
+ "success": False,
579
+ "error": f"Content too large: {size_mb:.1f}MB (max {self.max_create_size_mb}MB)"
580
+ }
581
+
582
+ # Create parent directories if needed
583
+ parent_dir = os.path.dirname(filepath)
584
+ if parent_dir and not os.path.exists(parent_dir):
585
+ if self.create_parent_directories:
586
+ try:
587
+ os.makedirs(parent_dir, exist_ok=True)
588
+ except Exception as e:
589
+ return {
590
+ "success": False,
591
+ "error": f"Failed to create parent directories: {str(e)}"
592
+ }
593
+
594
+ # Write file
595
+ try:
596
+ with open(filepath, 'w', encoding='utf-8') as f:
597
+ f.write(content)
598
+
599
+ os.chmod(filepath, 0o644)
600
+
601
+ size_bytes = len(content.encode('utf-8'))
602
+ output = f"✓ Created/overwrote {filepath} ({size_bytes} bytes)"
603
+ if backup_path:
604
+ output += f"\nBackup: {backup_path}"
605
+
606
+ return {
607
+ "success": True,
608
+ "output": output
609
+ }
610
+ except Exception as e:
611
+ # Restore from backup if failed
612
+ if backup_path and os.path.exists(backup_path):
613
+ shutil.copy2(backup_path, filepath)
614
+ return {
615
+ "success": False,
616
+ "error": f"Failed to write file: {str(e)}"
617
+ }
618
+
619
+ def _execute_delete(self, operation: Dict[str, Any]) -> Dict[str, Any]:
620
+ """Execute file delete operation.
621
+
622
+ Creates backup before deletion.
623
+
624
+ Args:
625
+ operation: Operation data
626
+
627
+ Returns:
628
+ Result dictionary
629
+ """
630
+ filepath = operation.get("file")
631
+
632
+ # Validation
633
+ if not filepath:
634
+ return {"success": False, "error": "Missing file path"}
635
+
636
+ is_valid, error = self.validate_file_path(filepath)
637
+ if not is_valid:
638
+ return {"success": False, "error": error}
639
+
640
+ if not os.path.exists(filepath):
641
+ return {
642
+ "success": False,
643
+ "error": f"File not found: {filepath}"
644
+ }
645
+
646
+ # Check protected paths
647
+ if self.is_protected_path(filepath):
648
+ return {
649
+ "success": False,
650
+ "error": f"Cannot delete protected file: {filepath}"
651
+ }
652
+
653
+ # Create backup with .deleted suffix
654
+ backup_path = self.create_backup(filepath, suffix=".deleted")
655
+
656
+ # Delete file
657
+ try:
658
+ os.remove(filepath)
659
+
660
+ output = f"✓ Deleted {filepath}"
661
+ if backup_path:
662
+ output += f"\nBackup: {backup_path}"
663
+
664
+ return {
665
+ "success": True,
666
+ "output": output
667
+ }
668
+ except Exception as e:
669
+ return {
670
+ "success": False,
671
+ "error": f"Failed to delete file: {str(e)}"
672
+ }
673
+
674
+ def _execute_move(self, operation: Dict[str, Any]) -> Dict[str, Any]:
675
+ """Execute file move operation.
676
+
677
+ Args:
678
+ operation: Operation data
679
+
680
+ Returns:
681
+ Result dictionary
682
+ """
683
+ from_path = operation.get("from")
684
+ to_path = operation.get("to")
685
+
686
+ # Validation
687
+ if not from_path or not to_path:
688
+ return {"success": False, "error": "Missing from/to paths"}
689
+
690
+ is_valid, error = self.validate_file_path(from_path)
691
+ if not is_valid:
692
+ return {"success": False, "error": f"Source: {error}"}
693
+
694
+ is_valid, error = self.validate_file_path(to_path)
695
+ if not is_valid:
696
+ return {"success": False, "error": f"Destination: {error}"}
697
+
698
+ if not os.path.exists(from_path):
699
+ return {
700
+ "success": False,
701
+ "error": f"Source file not found: {from_path}"
702
+ }
703
+
704
+ if os.path.exists(to_path):
705
+ return {
706
+ "success": False,
707
+ "error": f"Destination already exists: {to_path}. Delete it first or choose different destination."
708
+ }
709
+
710
+ if from_path == to_path:
711
+ return {
712
+ "success": False,
713
+ "error": f"Source and destination are the same: {from_path}"
714
+ }
715
+
716
+ # Create parent directories for destination if needed
717
+ parent_dir = os.path.dirname(to_path)
718
+ if parent_dir and not os.path.exists(parent_dir):
719
+ if self.create_parent_directories:
720
+ try:
721
+ os.makedirs(parent_dir, exist_ok=True)
722
+ except Exception as e:
723
+ return {
724
+ "success": False,
725
+ "error": f"Failed to create destination directory: {str(e)}"
726
+ }
727
+
728
+ # Move file
729
+ try:
730
+ shutil.move(from_path, to_path)
731
+
732
+ return {
733
+ "success": True,
734
+ "output": f"✓ Moved {from_path} → {to_path}"
735
+ }
736
+ except Exception as e:
737
+ return {
738
+ "success": False,
739
+ "error": f"Failed to move file: {str(e)}"
740
+ }
741
+
742
+ def _execute_copy(self, operation: Dict[str, Any]) -> Dict[str, Any]:
743
+ """Execute file copy operation.
744
+
745
+ Args:
746
+ operation: Operation data
747
+
748
+ Returns:
749
+ Result dictionary
750
+ """
751
+ from_path = operation.get("from")
752
+ to_path = operation.get("to")
753
+
754
+ # Validation
755
+ if not from_path or not to_path:
756
+ return {"success": False, "error": "Missing from/to paths"}
757
+
758
+ is_valid, error = self.validate_file_path(from_path)
759
+ if not is_valid:
760
+ return {"success": False, "error": f"Source: {error}"}
761
+
762
+ is_valid, error = self.validate_file_path(to_path)
763
+ if not is_valid:
764
+ return {"success": False, "error": f"Destination: {error}"}
765
+
766
+ if not os.path.exists(from_path):
767
+ return {
768
+ "success": False,
769
+ "error": f"Source file not found: {from_path}"
770
+ }
771
+
772
+ if os.path.exists(to_path):
773
+ return {
774
+ "success": False,
775
+ "error": f"Destination already exists: {to_path}. Use <copy_overwrite> to replace."
776
+ }
777
+
778
+ # Create parent directories for destination if needed
779
+ parent_dir = os.path.dirname(to_path)
780
+ if parent_dir and not os.path.exists(parent_dir):
781
+ if self.create_parent_directories:
782
+ try:
783
+ os.makedirs(parent_dir, exist_ok=True)
784
+ except Exception as e:
785
+ return {
786
+ "success": False,
787
+ "error": f"Failed to create destination directory: {str(e)}"
788
+ }
789
+
790
+ # Copy file with metadata
791
+ try:
792
+ shutil.copy2(from_path, to_path)
793
+
794
+ return {
795
+ "success": True,
796
+ "output": f"✓ Copied {from_path} → {to_path}"
797
+ }
798
+ except Exception as e:
799
+ return {
800
+ "success": False,
801
+ "error": f"Failed to copy file: {str(e)}"
802
+ }
803
+
804
+ def _execute_copy_overwrite(self, operation: Dict[str, Any]) -> Dict[str, Any]:
805
+ """Execute file copy with overwrite.
806
+
807
+ Args:
808
+ operation: Operation data
809
+
810
+ Returns:
811
+ Result dictionary
812
+ """
813
+ from_path = operation.get("from")
814
+ to_path = operation.get("to")
815
+
816
+ # Validation
817
+ if not from_path or not to_path:
818
+ return {"success": False, "error": "Missing from/to paths"}
819
+
820
+ is_valid, error = self.validate_file_path(from_path)
821
+ if not is_valid:
822
+ return {"success": False, "error": f"Source: {error}"}
823
+
824
+ is_valid, error = self.validate_file_path(to_path)
825
+ if not is_valid:
826
+ return {"success": False, "error": f"Destination: {error}"}
827
+
828
+ if not os.path.exists(from_path):
829
+ return {
830
+ "success": False,
831
+ "error": f"Source file not found: {from_path}"
832
+ }
833
+
834
+ # Create backup if destination exists
835
+ backup_path = None
836
+ if os.path.exists(to_path):
837
+ backup_path = self.create_backup(to_path)
838
+
839
+ # Create parent directories for destination if needed
840
+ parent_dir = os.path.dirname(to_path)
841
+ if parent_dir and not os.path.exists(parent_dir):
842
+ if self.create_parent_directories:
843
+ try:
844
+ os.makedirs(parent_dir, exist_ok=True)
845
+ except Exception as e:
846
+ return {
847
+ "success": False,
848
+ "error": f"Failed to create destination directory: {str(e)}"
849
+ }
850
+
851
+ # Copy file with metadata
852
+ try:
853
+ shutil.copy2(from_path, to_path)
854
+
855
+ output = f"✓ Copied {from_path} → {to_path}"
856
+ if backup_path:
857
+ output += f"\nBackup: {backup_path}"
858
+
859
+ return {
860
+ "success": True,
861
+ "output": output
862
+ }
863
+ except Exception as e:
864
+ # Restore backup if failed
865
+ if backup_path and os.path.exists(backup_path):
866
+ shutil.copy2(backup_path, to_path)
867
+ return {
868
+ "success": False,
869
+ "error": f"Failed to copy file: {str(e)}"
870
+ }
871
+
872
+ def _execute_append(self, operation: Dict[str, Any]) -> Dict[str, Any]:
873
+ """Execute file append operation.
874
+
875
+ Args:
876
+ operation: Operation data
877
+
878
+ Returns:
879
+ Result dictionary
880
+ """
881
+ filepath = operation.get("file")
882
+ content = operation.get("content", "")
883
+
884
+ # Validation
885
+ if not filepath:
886
+ return {"success": False, "error": "Missing file path"}
887
+
888
+ if not content:
889
+ return {"success": False, "error": "Empty content"}
890
+
891
+ is_valid, error = self.validate_file_path(filepath)
892
+ if not is_valid:
893
+ return {"success": False, "error": error}
894
+
895
+ if not os.path.exists(filepath):
896
+ return {
897
+ "success": False,
898
+ "error": f"File not found: {filepath}"
899
+ }
900
+
901
+ # Check if text file
902
+ if not self.is_text_file(filepath):
903
+ return {
904
+ "success": False,
905
+ "error": f"Cannot append to binary file: {filepath}"
906
+ }
907
+
908
+ # Create backup
909
+ backup_path = self.create_backup(filepath)
910
+
911
+ # Append content
912
+ try:
913
+ with open(filepath, 'a', encoding='utf-8') as f:
914
+ f.write(content)
915
+
916
+ output = f"✓ Appended content to {filepath}"
917
+ if backup_path:
918
+ output += f"\nBackup: {backup_path}"
919
+
920
+ return {
921
+ "success": True,
922
+ "output": output
923
+ }
924
+ except Exception as e:
925
+ # Restore backup if failed
926
+ if backup_path and os.path.exists(backup_path):
927
+ shutil.copy2(backup_path, filepath)
928
+ return {
929
+ "success": False,
930
+ "error": f"Failed to append to file: {str(e)}"
931
+ }
932
+
933
+ def _execute_insert_after(self, operation: Dict[str, Any]) -> Dict[str, Any]:
934
+ """Execute insert after pattern operation.
935
+
936
+ Pattern must appear exactly once (fails on 0 or 2+ matches).
937
+
938
+ Args:
939
+ operation: Operation data
940
+
941
+ Returns:
942
+ Result dictionary
943
+ """
944
+ filepath = operation.get("file")
945
+ pattern = operation.get("pattern")
946
+ content = operation.get("content", "")
947
+
948
+ # Validation
949
+ if not filepath or not pattern:
950
+ return {"success": False, "error": "Missing file or pattern"}
951
+
952
+ is_valid, error = self.validate_file_path(filepath)
953
+ if not is_valid:
954
+ return {"success": False, "error": error}
955
+
956
+ if not os.path.exists(filepath):
957
+ return {
958
+ "success": False,
959
+ "error": f"File not found: {filepath}"
960
+ }
961
+
962
+ # Check if text file
963
+ if not self.is_text_file(filepath):
964
+ return {
965
+ "success": False,
966
+ "error": f"Cannot insert into binary file: {filepath}"
967
+ }
968
+
969
+ # Read file
970
+ try:
971
+ with open(filepath, 'r', encoding='utf-8') as f:
972
+ file_content = f.read()
973
+ except Exception as e:
974
+ return {
975
+ "success": False,
976
+ "error": f"Failed to read file: {str(e)}"
977
+ }
978
+
979
+ # Find pattern occurrences
980
+ count = file_content.count(pattern)
981
+ line_numbers = self.find_pattern_occurrences(file_content, pattern)
982
+
983
+ # Validate exact match
984
+ if count == 0:
985
+ return {
986
+ "success": False,
987
+ "error": f"Pattern not found: '{pattern}'"
988
+ }
989
+ elif count > 1:
990
+ lines_str = ", ".join(str(ln) for ln in line_numbers[:10])
991
+ return {
992
+ "success": False,
993
+ "error": f"Ambiguous pattern: '{pattern}' appears {count} times at lines {lines_str}. "
994
+ f"Pattern must be unique for insert operations. Use <edit> with full context instead."
995
+ }
996
+
997
+ # Create backup
998
+ backup_path = self.create_backup(filepath)
999
+
1000
+ # Insert content after pattern
1001
+ new_content = file_content.replace(pattern, f"{pattern}\n{content}", 1)
1002
+
1003
+ # Write back
1004
+ try:
1005
+ with open(filepath, 'w', encoding='utf-8') as f:
1006
+ f.write(new_content)
1007
+
1008
+ output = f"✓ Inserted content after pattern in {filepath} (line {line_numbers[0]})"
1009
+ if backup_path:
1010
+ output += f"\nBackup: {backup_path}"
1011
+
1012
+ return {
1013
+ "success": True,
1014
+ "output": output
1015
+ }
1016
+ except Exception as e:
1017
+ # Restore backup if failed
1018
+ if backup_path and os.path.exists(backup_path):
1019
+ shutil.copy2(backup_path, filepath)
1020
+ return {
1021
+ "success": False,
1022
+ "error": f"Failed to write file: {str(e)}"
1023
+ }
1024
+
1025
+ def _execute_insert_before(self, operation: Dict[str, Any]) -> Dict[str, Any]:
1026
+ """Execute insert before pattern operation.
1027
+
1028
+ Pattern must appear exactly once (fails on 0 or 2+ matches).
1029
+
1030
+ Args:
1031
+ operation: Operation data
1032
+
1033
+ Returns:
1034
+ Result dictionary
1035
+ """
1036
+ filepath = operation.get("file")
1037
+ pattern = operation.get("pattern")
1038
+ content = operation.get("content", "")
1039
+
1040
+ # Validation
1041
+ if not filepath or not pattern:
1042
+ return {"success": False, "error": "Missing file or pattern"}
1043
+
1044
+ is_valid, error = self.validate_file_path(filepath)
1045
+ if not is_valid:
1046
+ return {"success": False, "error": error}
1047
+
1048
+ if not os.path.exists(filepath):
1049
+ return {
1050
+ "success": False,
1051
+ "error": f"File not found: {filepath}"
1052
+ }
1053
+
1054
+ # Check if text file
1055
+ if not self.is_text_file(filepath):
1056
+ return {
1057
+ "success": False,
1058
+ "error": f"Cannot insert into binary file: {filepath}"
1059
+ }
1060
+
1061
+ # Read file
1062
+ try:
1063
+ with open(filepath, 'r', encoding='utf-8') as f:
1064
+ file_content = f.read()
1065
+ except Exception as e:
1066
+ return {
1067
+ "success": False,
1068
+ "error": f"Failed to read file: {str(e)}"
1069
+ }
1070
+
1071
+ # Find pattern occurrences
1072
+ count = file_content.count(pattern)
1073
+ line_numbers = self.find_pattern_occurrences(file_content, pattern)
1074
+
1075
+ # Validate exact match
1076
+ if count == 0:
1077
+ return {
1078
+ "success": False,
1079
+ "error": f"Pattern not found: '{pattern}'"
1080
+ }
1081
+ elif count > 1:
1082
+ lines_str = ", ".join(str(ln) for ln in line_numbers[:10])
1083
+ return {
1084
+ "success": False,
1085
+ "error": f"Ambiguous pattern: '{pattern}' appears {count} times at lines {lines_str}. "
1086
+ f"Pattern must be unique for insert operations. Use <edit> with full context instead."
1087
+ }
1088
+
1089
+ # Create backup
1090
+ backup_path = self.create_backup(filepath)
1091
+
1092
+ # Insert content before pattern
1093
+ new_content = file_content.replace(pattern, f"{content}\n{pattern}", 1)
1094
+
1095
+ # Write back
1096
+ try:
1097
+ with open(filepath, 'w', encoding='utf-8') as f:
1098
+ f.write(new_content)
1099
+
1100
+ output = f"✓ Inserted content before pattern in {filepath} (line {line_numbers[0]})"
1101
+ if backup_path:
1102
+ output += f"\nBackup: {backup_path}"
1103
+
1104
+ return {
1105
+ "success": True,
1106
+ "output": output
1107
+ }
1108
+ except Exception as e:
1109
+ # Restore backup if failed
1110
+ if backup_path and os.path.exists(backup_path):
1111
+ shutil.copy2(backup_path, filepath)
1112
+ return {
1113
+ "success": False,
1114
+ "error": f"Failed to write file: {str(e)}"
1115
+ }
1116
+
1117
+ def _execute_mkdir(self, operation: Dict[str, Any]) -> Dict[str, Any]:
1118
+ """Execute create directory operation.
1119
+
1120
+ Args:
1121
+ operation: Operation data
1122
+
1123
+ Returns:
1124
+ Result dictionary
1125
+ """
1126
+ dir_path = operation.get("path")
1127
+
1128
+ # Validation
1129
+ if not dir_path:
1130
+ return {"success": False, "error": "Missing directory path"}
1131
+
1132
+ is_valid, error = self.validate_file_path(dir_path)
1133
+ if not is_valid:
1134
+ return {"success": False, "error": error}
1135
+
1136
+ if os.path.exists(dir_path):
1137
+ if os.path.isdir(dir_path):
1138
+ return {
1139
+ "success": False,
1140
+ "error": f"Directory already exists: {dir_path}"
1141
+ }
1142
+ else:
1143
+ return {
1144
+ "success": False,
1145
+ "error": f"Path exists as a file: {dir_path}"
1146
+ }
1147
+
1148
+ # Create directory with parents
1149
+ try:
1150
+ os.makedirs(dir_path, mode=0o755, exist_ok=False)
1151
+
1152
+ return {
1153
+ "success": True,
1154
+ "output": f"✓ Created directory: {dir_path}"
1155
+ }
1156
+ except Exception as e:
1157
+ return {
1158
+ "success": False,
1159
+ "error": f"Failed to create directory: {str(e)}"
1160
+ }
1161
+
1162
+ def _execute_rmdir(self, operation: Dict[str, Any]) -> Dict[str, Any]:
1163
+ """Execute remove directory operation.
1164
+
1165
+ Only removes empty directories.
1166
+
1167
+ Args:
1168
+ operation: Operation data
1169
+
1170
+ Returns:
1171
+ Result dictionary
1172
+ """
1173
+ dir_path = operation.get("path")
1174
+
1175
+ # Validation
1176
+ if not dir_path:
1177
+ return {"success": False, "error": "Missing directory path"}
1178
+
1179
+ is_valid, error = self.validate_file_path(dir_path)
1180
+ if not is_valid:
1181
+ return {"success": False, "error": error}
1182
+
1183
+ if not os.path.exists(dir_path):
1184
+ return {
1185
+ "success": False,
1186
+ "error": f"Directory not found: {dir_path}"
1187
+ }
1188
+
1189
+ if not os.path.isdir(dir_path):
1190
+ return {
1191
+ "success": False,
1192
+ "error": f"Path is not a directory: {dir_path}"
1193
+ }
1194
+
1195
+ # Check protected paths
1196
+ if self.is_protected_path(dir_path):
1197
+ return {
1198
+ "success": False,
1199
+ "error": f"Cannot delete protected directory: {dir_path}"
1200
+ }
1201
+
1202
+ # Check if directory is empty
1203
+ try:
1204
+ if os.listdir(dir_path):
1205
+ return {
1206
+ "success": False,
1207
+ "error": f"Directory not empty: {dir_path}. Only empty directories can be removed."
1208
+ }
1209
+ except Exception as e:
1210
+ return {
1211
+ "success": False,
1212
+ "error": f"Failed to check directory: {str(e)}"
1213
+ }
1214
+
1215
+ # Remove directory
1216
+ try:
1217
+ os.rmdir(dir_path)
1218
+
1219
+ return {
1220
+ "success": True,
1221
+ "output": f"✓ Removed directory: {dir_path}"
1222
+ }
1223
+ except Exception as e:
1224
+ return {
1225
+ "success": False,
1226
+ "error": f"Failed to remove directory: {str(e)}"
1227
+ }
1228
+
1229
+ def _execute_read(self, operation: Dict[str, Any]) -> Dict[str, Any]:
1230
+ """Execute read file operation.
1231
+
1232
+ Args:
1233
+ operation: Operation data
1234
+
1235
+ Returns:
1236
+ Result dictionary with file content
1237
+ """
1238
+ filepath = operation.get("file")
1239
+ lines_spec = operation.get("lines") # Optional: "10-20"
1240
+
1241
+ # Validation
1242
+ if not filepath:
1243
+ return {"success": False, "error": "Missing file path"}
1244
+
1245
+ is_valid, error = self.validate_file_path(filepath)
1246
+ if not is_valid:
1247
+ return {"success": False, "error": error}
1248
+
1249
+ if not os.path.exists(filepath):
1250
+ return {
1251
+ "success": False,
1252
+ "error": f"File not found: {filepath}"
1253
+ }
1254
+
1255
+ # Check file size
1256
+ is_valid, error = self.check_file_size(filepath, self.max_read_size_mb)
1257
+ if not is_valid:
1258
+ return {"success": False, "error": error}
1259
+
1260
+ # Check if text file
1261
+ if not self.is_text_file(filepath):
1262
+ return {
1263
+ "success": False,
1264
+ "error": f"Cannot read binary file: {filepath}"
1265
+ }
1266
+
1267
+ # Read file
1268
+ try:
1269
+ with open(filepath, 'r', encoding='utf-8') as f:
1270
+ content = f.read()
1271
+ except Exception as e:
1272
+ return {
1273
+ "success": False,
1274
+ "error": f"Failed to read file: {str(e)}"
1275
+ }
1276
+
1277
+ # Count total lines for display
1278
+ total_lines = content.count('\n') + 1 if content else 0
1279
+
1280
+ # Handle line range if specified
1281
+ if lines_spec:
1282
+ try:
1283
+ if '-' in lines_spec:
1284
+ start_str, end_str = lines_spec.split('-')
1285
+ start_line = int(start_str.strip()) - 1 # Convert to 0-indexed
1286
+ end_line = int(end_str.strip())
1287
+ else:
1288
+ start_line = int(lines_spec.strip()) - 1
1289
+ end_line = start_line + 1
1290
+
1291
+ lines = content.split('\n')
1292
+ selected_lines = lines[start_line:end_line]
1293
+ content = '\n'.join(selected_lines)
1294
+ line_count = len(selected_lines)
1295
+
1296
+ return {
1297
+ "success": True,
1298
+ "output": f"✓ Read {line_count} lines from {filepath} (lines {lines_spec}):\n\n{content}"
1299
+ }
1300
+ except Exception as e:
1301
+ return {
1302
+ "success": False,
1303
+ "error": f"Invalid line specification '{lines_spec}': {str(e)}"
1304
+ }
1305
+
1306
+ return {
1307
+ "success": True,
1308
+ "output": f"✓ Read {total_lines} lines from {filepath}:\n\n{content}"
1309
+ }
1310
+
1311
+ def _execute_grep(self, operation: Dict[str, Any]) -> Dict[str, Any]:
1312
+ """Execute grep file operation (search for pattern in file).
1313
+
1314
+ Args:
1315
+ operation: Operation data
1316
+
1317
+ Returns:
1318
+ Result dictionary with matching lines
1319
+ """
1320
+ filepath = operation.get("file")
1321
+ pattern = operation.get("pattern")
1322
+ case_insensitive = operation.get("case_insensitive", False)
1323
+
1324
+ # Validation
1325
+ if not filepath:
1326
+ return {"success": False, "error": "Missing file path"}
1327
+
1328
+ if not pattern:
1329
+ return {"success": False, "error": "Missing search pattern"}
1330
+
1331
+ is_valid, error = self.validate_file_path(filepath)
1332
+ if not is_valid:
1333
+ return {"success": False, "error": error}
1334
+
1335
+ if not os.path.exists(filepath):
1336
+ return {
1337
+ "success": False,
1338
+ "error": f"File not found: {filepath}"
1339
+ }
1340
+
1341
+ # Check file size
1342
+ is_valid, error = self.check_file_size(filepath, self.max_read_size_mb)
1343
+ if not is_valid:
1344
+ return {"success": False, "error": error}
1345
+
1346
+ # Check if text file
1347
+ if not self.is_text_file(filepath):
1348
+ return {
1349
+ "success": False,
1350
+ "error": f"Cannot grep binary file: {filepath}"
1351
+ }
1352
+
1353
+ # Read file and search for pattern
1354
+ try:
1355
+ with open(filepath, 'r', encoding='utf-8') as f:
1356
+ lines = f.readlines()
1357
+ except Exception as e:
1358
+ return {
1359
+ "success": False,
1360
+ "error": f"Failed to read file: {str(e)}"
1361
+ }
1362
+
1363
+ # Search for pattern in each line
1364
+ import re
1365
+ matches = []
1366
+ flags = re.IGNORECASE if case_insensitive else 0
1367
+
1368
+ try:
1369
+ regex = re.compile(pattern, flags)
1370
+ except re.error as e:
1371
+ return {
1372
+ "success": False,
1373
+ "error": f"Invalid regex pattern: {str(e)}"
1374
+ }
1375
+
1376
+ for line_num, line in enumerate(lines, start=1):
1377
+ if regex.search(line):
1378
+ # Remove trailing newline for cleaner display
1379
+ matches.append((line_num, line.rstrip('\n')))
1380
+
1381
+ # Build result output
1382
+ if not matches:
1383
+ return {
1384
+ "success": True,
1385
+ "output": f"✓ No matches found for '{pattern}' in {filepath}"
1386
+ }
1387
+
1388
+ match_count = len(matches)
1389
+ result_lines = [f"✓ Found {match_count} match{'es' if match_count != 1 else ''} for '{pattern}' in {filepath}:\n"]
1390
+
1391
+ # Show up to 50 matches
1392
+ for line_num, line_content in matches[:50]:
1393
+ result_lines.append(f"{line_num}: {line_content}")
1394
+
1395
+ if len(matches) > 50:
1396
+ result_lines.append(f"\n... ({len(matches) - 50} more matches)")
1397
+
1398
+ return {
1399
+ "success": True,
1400
+ "output": "\n".join(result_lines)
1401
+ }