1- """
2- Editor tool designed to do changes iteratively on multiple files.
1+ """Editor tool designed to do changes iteratively on multiple files.
32
43This module provides a comprehensive file and code editor with rich output formatting,
54syntax highlighting, and intelligent text manipulation capabilities. It's designed for
7271import os
7372import re
7473import shutil
75- from typing import Any , Optional
74+ from typing import Any , Dict , List , Optional , Union
7675
7776from rich import box
7877from rich .panel import Panel
7978from rich .syntax import Syntax
8079from rich .table import Table
8180from rich .text import Text
8281from rich .tree import Tree
83- from strands . types . tools import ToolResult , ToolUse
82+ from strands import tool
8483
8584from strands_tools .utils import console_util
8685from strands_tools .utils .detect_language import detect_language
@@ -136,132 +135,6 @@ def validate_pattern(pattern: str) -> bool:
136135 return False
137136
138137
139- TOOL_SPEC = {
140- "name" : "editor" ,
141- "description" : "Editor tool designed to do changes iteratively on multiple files.\n \n "
142- "IMPORTANT ERROR PREVENTION:\n "
143- "1. Required Parameters:\n "
144- " • file_text: REQUIRED for 'create' command - content of file to create\n "
145- " • search_text: REQUIRED for 'find_line' command - text to search\n "
146- " • insert command: BOTH new_str AND insert_line REQUIRED\n \n "
147- "2. Command-Specific Requirements:\n "
148- " • create: Must provide file_text, file_text is required for create command\n "
149- " • str_replace: Both old_str and new_str are required for str_replace command\n "
150- " • pattern_replace: Both pattern and new_str required\n "
151- " • insert: Both new_str and insert_line required\n "
152- " • find_line: search_text required\n \n "
153- "3. Path Handling:\n "
154- " • Use absolute paths (e.g., /Users/name/file.txt)\n "
155- " • Or user-relative paths (~/folder/file.txt)\n "
156- " • Ensure parent directories exist for create command\n \n "
157- "Core Features:\n \n "
158- "1. Rich Text Display:\n "
159- " • Syntax highlighting (Python, JavaScript, Java, HTML, etc.)\n "
160- " • Line numbering and code formatting\n "
161- " • Interactive directory trees with icons\n "
162- " • Beautiful console output with panels and tables\n \n "
163- "2. File Operations:\n "
164- " • View: Smart file content display with syntax highlighting\n "
165- " • Create: New file creation with proper directory handling\n "
166- " • Replace: Precise string and pattern-based replacement\n "
167- " • Insert: Smart line finding and content insertion\n "
168- " • Undo: Automatic backup and restore capability\n \n "
169- "3. Smart Features:\n "
170- " • Content History: Caches file contents to reduce reads\n "
171- " • Pattern Matching: Regex-based replacements\n "
172- " • Smart Line Finding: Context-aware line location\n "
173- " • Fuzzy Search: Flexible text matching\n \n "
174- "4. Safety Features:\n "
175- " • Automatic backup creation before modifications\n "
176- " • Content caching for performance\n "
177- " • Error prevention and validation\n "
178- " • One-step undo functionality\n \n "
179- "Example Usage:\n "
180- "1. Create file: command='create', path='/path/to/file.txt', file_text='content'\n "
181- "2. View file: command='view', path='/path/to/file.txt'\n "
182- "3. Insert text: command='insert', path='/path/to/file.txt', new_str='text', insert_line=5\n "
183- "4. Replace text: command='str_replace', path='/file.txt', old_str='old', new_str='new'\n "
184- "5. Find line: command='find_line', path='/file.txt', search_text='find this'\n "
185- "6. Undo change: command='undo_edit', path='/path/to/file.txt'" ,
186- "inputSchema" : {
187- "json" : {
188- "type" : "object" ,
189- "properties" : {
190- "command" : {
191- "type" : "string" ,
192- "enum" : [
193- "view" ,
194- "create" ,
195- "str_replace" ,
196- "pattern_replace" ,
197- "insert" ,
198- "find_line" ,
199- "undo_edit" ,
200- ],
201- "description" : (
202- "The commands to run: `view`, `create`, `str_replace`, `pattern_replace`, "
203- "`insert`, `find_line`, `undo_edit`."
204- ),
205- },
206- "path" : {
207- "description" : "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`." ,
208- "type" : "string" ,
209- },
210- "file_text" : {
211- "description" : (
212- "Required parameter of `create` command, with the content of the file to be created."
213- ),
214- "type" : "string" ,
215- },
216- "insert_line" : {
217- "description" : (
218- "Required parameter of `insert` command. The `new_str` will be inserted AFTER "
219- "the line `insert_line` of `path`. Can be a line number or search text."
220- ),
221- "type" : "string" ,
222- },
223- "new_str" : {
224- "description" : (
225- "Required parameter containing the new string for `str_replace`, "
226- "`pattern_replace` or `insert` commands."
227- ),
228- "type" : "string" ,
229- },
230- "old_str" : {
231- "description" : (
232- "Required parameter of `str_replace` command containing the exact string to replace."
233- ),
234- "type" : "string" ,
235- },
236- "pattern" : {
237- "description" : (
238- "Required parameter of `pattern_replace` command containing the regex pattern to match."
239- ),
240- "type" : "string" ,
241- },
242- "search_text" : {
243- "description" : "Text to search for in `find_line` command. Supports fuzzy matching." ,
244- "type" : "string" ,
245- },
246- "fuzzy" : {
247- "description" : "Enable fuzzy matching for `find_line` command." ,
248- "type" : "boolean" ,
249- },
250- "view_range" : {
251- "description" : (
252- "Optional parameter of `view` command. Line range to show [start, end]. "
253- "Supports negative indices."
254- ),
255- "items" : {"type" : "integer" },
256- "type" : "array" ,
257- },
258- },
259- "required" : ["command" , "path" ],
260- }
261- },
262- }
263-
264-
265138def format_code (code : str , language : str ) -> Syntax :
266139 """Format code using Rich syntax highlighting."""
267140 syntax = Syntax (code , language , theme = "monokai" , line_numbers = True )
@@ -307,7 +180,19 @@ def format_output(title: str, content: Any, style: str = "default") -> Panel:
307180 return panel
308181
309182
310- def editor (tool : ToolUse , ** kwargs : Any ) -> ToolResult :
183+ @tool
184+ def editor (
185+ command : str ,
186+ path : str ,
187+ file_text : Optional [str ] = None ,
188+ insert_line : Optional [Union [str , int ]] = None ,
189+ new_str : Optional [str ] = None ,
190+ old_str : Optional [str ] = None ,
191+ pattern : Optional [str ] = None ,
192+ search_text : Optional [str ] = None ,
193+ fuzzy : bool = False ,
194+ view_range : Optional [List [int ]] = None ,
195+ ) -> Dict [str , Any ]:
311196 """
312197 Editor tool designed to do changes iteratively on multiple files.
313198
@@ -429,14 +314,15 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
429314 console = console_util .create ()
430315
431316 try :
432- tool_use_id = tool .get ("toolUseId" , "default-id" )
433- tool_input = tool .get ("input" , {})
317+ path = os .path .expanduser (path )
434318
435- command = tool_input . get ( " command" )
436- path = os . path . expanduser ( tool_input . get ( "path" , "" ) )
319+ if not command :
320+ raise ValueError ( "Command is required" )
437321
438- if not command or not path :
439- raise ValueError ("Both command and path are required" )
322+ # Validate command
323+ valid_commands = ["view" , "create" , "str_replace" , "pattern_replace" , "insert" , "find_line" , "undo_edit" ]
324+ if command not in valid_commands :
325+ raise ValueError (f"Unknown command: { command } . Valid commands: { ', ' .join (valid_commands )} " )
440326
441327 # Get environment variables at runtime
442328 editor_dir_tree_max_depth = int (os .getenv ("EDITOR_DIR_TREE_MAX_DEPTH" , "2" ))
@@ -455,7 +341,9 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
455341
456342 # Preview specific changes for each command
457343 if command == "create" :
458- content = tool_input .get ("file_text" , "" )
344+ if not file_text :
345+ raise ValueError ("file_text is required for create command" )
346+ content = file_text
459347 language = detect_language (path )
460348 # Use Syntax directly for proper highlighting
461349 syntax = Syntax (content , language , theme = "monokai" , line_numbers = True )
@@ -468,8 +356,11 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
468356 )
469357 )
470358 elif command in {"str_replace" , "pattern_replace" }:
471- old = tool_input .get ("old_str" if command == "str_replace" else "pattern" , "" )
472- new = tool_input .get ("new_str" , "" )
359+ old = old_str if command == "str_replace" else pattern
360+ new = new_str
361+ if not old or not new :
362+ param_name = "old_str" if command == "str_replace" else "pattern"
363+ raise ValueError (f"Both { param_name } and new_str are required for { command } command" )
473364 language = detect_language (path )
474365
475366 # Create table grid for side-by-side display
@@ -526,8 +417,8 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
526417 console .print (preview_panel )
527418 console .print ()
528419 elif command == "insert" :
529- new_str = tool_input . get ( " new_str" , "" )
530- insert_line = tool_input . get ( " insert_line" , " " )
420+ if not new_str or insert_line is None :
421+ raise ValueError ( "Both new_str and insert_line are required for insert command " )
531422 language = detect_language (path )
532423 # Create table with syntax highlighting
533424 table = Table (title = "Insertion Preview" , show_header = True )
@@ -559,7 +450,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
559450 )
560451 console .print (error_panel )
561452 return {
562- "toolUseId" : tool_use_id ,
563453 "status" : "error" ,
564454 "content" : [{"text" : error_message }],
565455 }
@@ -573,7 +463,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
573463 content = f .read ()
574464 save_content_history (path , content )
575465
576- view_range = tool_input .get ("view_range" )
577466 if view_range :
578467 lines = content .split ("\n " )
579468 start = max (0 , view_range [0 ] - 1 )
@@ -612,7 +501,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
612501 raise ValueError (f"Path { path } does not exist" )
613502
614503 elif command == "create" :
615- file_text = tool_input .get ("file_text" )
616504 if not file_text :
617505 raise ValueError ("file_text is required for create command" )
618506
@@ -627,9 +515,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
627515 result = f"File { path } created successfully"
628516
629517 elif command == "str_replace" :
630- old_str = tool_input .get ("old_str" )
631- new_str = tool_input .get ("new_str" )
632-
633518 if not old_str or not new_str :
634519 raise ValueError ("Both old_str and new_str are required for str_replace command" )
635520
@@ -645,7 +530,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
645530 if count == 0 :
646531 # Return existing content if no matches
647532 return {
648- "toolUseId" : tool_use_id ,
649533 "status" : "error" ,
650534 "content" : [{"text" : f"Note: old_str not found in { path } . Current content:\n { content } " }],
651535 }
@@ -660,25 +544,13 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
660544 f .write (new_content )
661545 save_content_history (path , new_content )
662546
663- # First check if we actually have matches
664- count = content .count (old_str )
665- if count == 0 :
666- return {
667- "toolUseId" : tool_use_id ,
668- "status" : "error" ,
669- "content" : [{"text" : f"Note: old_str not found in { path } . Current content:\n { content } " }],
670- }
671-
672547 result = (
673548 f"Text replacement complete and details displayed in console.\n File: { path } \n "
674549 f"Replaced { count } occurrence{ 's' if count > 1 else '' } \n "
675550 f"Old string: { old_str } \n New string: { new_str } \n "
676551 )
677552
678553 elif command == "pattern_replace" :
679- pattern = tool_input .get ("pattern" )
680- new_str = tool_input .get ("new_str" )
681-
682554 if not pattern or not new_str :
683555 raise ValueError ("Both pattern and new_str are required for pattern_replace command" )
684556
@@ -698,7 +570,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
698570 matches = list (regex .finditer (content ))
699571 if not matches :
700572 return {
701- "toolUseId" : tool_use_id ,
702573 "status" : "success" ,
703574 "content" : [{"text" : f"Note: pattern '{ pattern } ' not found in { path } . Current content:{ content } " }],
704575 }
@@ -723,7 +594,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
723594
724595 before = content [context_start :start ]
725596 matched = content [start :end ]
726- after = content [end :context_end ]
727597
728598 # Highlight the replacement
729599 preview = regex .sub (new_str , matched )
@@ -774,9 +644,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
774644 )
775645
776646 elif command == "insert" :
777- new_str = tool_input .get ("new_str" )
778- insert_line = tool_input .get ("insert_line" )
779-
780647 if not new_str or insert_line is None :
781648 raise ValueError ("Both new_str and insert_line are required for insert command" )
782649
@@ -791,11 +658,9 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
791658
792659 # Handle string-based line finding
793660 if isinstance (insert_line , str ):
794- fuzzy = tool_input .get ("fuzzy" , False )
795661 line_num = find_context_line (content , insert_line , fuzzy )
796662 if line_num == - 1 :
797663 return {
798- "toolUseId" : tool_use_id ,
799664 "status" : "success" ,
800665 "content" : [
801666 {
@@ -849,7 +714,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
849714 )
850715
851716 elif command == "find_line" :
852- search_text = tool_input .get ("search_text" )
853717 if not search_text :
854718 raise ValueError ("search_text is required for find_line command" )
855719
@@ -861,12 +725,10 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
861725 save_content_history (path , content )
862726
863727 # Find line
864- fuzzy = tool_input .get ("fuzzy" , False )
865728 line_num = find_context_line (content , search_text , fuzzy )
866729
867730 if line_num == - 1 :
868731 return {
869- "toolUseId" : tool_use_id ,
870732 "status" : "success" ,
871733 "content" : [
872734 {
@@ -924,7 +786,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
924786 raise ValueError (f"Unknown command: { command } " )
925787
926788 return {
927- "toolUseId" : tool_use_id ,
928789 "status" : "success" ,
929790 "content" : [{"text" : result }],
930791 }
@@ -933,7 +794,6 @@ def editor(tool: ToolUse, **kwargs: Any) -> ToolResult:
933794 error_msg = format_output ("❌ Error" , str (e ), "red" )
934795 console .print (error_msg )
935796 return {
936- "toolUseId" : tool_use_id if "tool_use_id" in locals () else "error-id" ,
937797 "status" : "error" ,
938798 "content" : [{"text" : f"Error: { str (e )} " }],
939799 }
0 commit comments