Skip to content

Commit 983ec41

Browse files
authored
feat: migrate editor tool from TOOL_SPEC to @tool decorator (#111)
1 parent 3d69fa2 commit 983ec41

File tree

2 files changed

+99
-315
lines changed

2 files changed

+99
-315
lines changed

src/strands_tools/editor.py

Lines changed: 33 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
Editor tool designed to do changes iteratively on multiple files.
1+
"""Editor tool designed to do changes iteratively on multiple files.
32
43
This module provides a comprehensive file and code editor with rich output formatting,
54
syntax highlighting, and intelligent text manipulation capabilities. It's designed for
@@ -72,15 +71,15 @@
7271
import os
7372
import re
7473
import shutil
75-
from typing import Any, Optional
74+
from typing import Any, Dict, List, Optional, Union
7675

7776
from rich import box
7877
from rich.panel import Panel
7978
from rich.syntax import Syntax
8079
from rich.table import Table
8180
from rich.text import Text
8281
from rich.tree import Tree
83-
from strands.types.tools import ToolResult, ToolUse
82+
from strands import tool
8483

8584
from strands_tools.utils import console_util
8685
from 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-
265138
def 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.\nFile: {path}\n"
674549
f"Replaced {count} occurrence{'s' if count > 1 else ''}\n"
675550
f"Old string: {old_str}\nNew 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

Comments
 (0)