Skip to content

Commit a4331b8

Browse files
committed
Add textEditor tool that combines readFile and updateFile functionality
1 parent 7838cea commit a4331b8

File tree

4 files changed

+653
-0
lines changed

4 files changed

+653
-0
lines changed

.changeset/text-editor.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mycoder-agent": minor
3+
---
4+
5+
Add textEditor tool that combines readFile and updateFile functionality

packages/agent/src/tools/getTools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { subAgentTool } from './interaction/subAgent.js';
66
import { userPromptTool } from './interaction/userPrompt.js';
77
import { fetchTool } from './io/fetch.js';
88
import { readFileTool } from './io/readFile.js';
9+
import { textEditorTool } from './io/textEditor.js';
910
import { updateFileTool } from './io/updateFile.js';
1011
import { respawnTool } from './system/respawn.js';
1112
import { sequenceCompleteTool } from './system/sequenceComplete.js';
@@ -15,6 +16,7 @@ import { sleepTool } from './system/sleep.js';
1516

1617
export function getTools(): Tool[] {
1718
return [
19+
textEditorTool,
1820
subAgentTool,
1921
readFileTool,
2022
updateFileTool,
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { randomUUID } from 'crypto';
2+
import { mkdtemp, readFile } from 'fs/promises';
3+
import { tmpdir } from 'os';
4+
import { join } from 'path';
5+
6+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7+
8+
import { TokenTracker } from '../../core/tokens.js';
9+
import { ToolContext } from '../../core/types.js';
10+
import { MockLogger } from '../../utils/mockLogger.js';
11+
import { shellExecuteTool } from '../system/shellExecute.js';
12+
13+
import { textEditorTool } from './textEditor.js';
14+
15+
const toolContext: ToolContext = {
16+
logger: new MockLogger(),
17+
headless: true,
18+
workingDirectory: '.',
19+
userSession: false,
20+
pageFilter: 'simple',
21+
tokenTracker: new TokenTracker(),
22+
};
23+
24+
describe('textEditor', () => {
25+
let testDir: string;
26+
27+
beforeEach(async () => {
28+
testDir = await mkdtemp(join(tmpdir(), 'texteditor-test-'));
29+
});
30+
31+
afterEach(async () => {
32+
await shellExecuteTool.execute(
33+
{ command: `rm -rf "${testDir}"`, description: 'test' },
34+
toolContext,
35+
);
36+
});
37+
38+
it('should create a file', async () => {
39+
const testContent = 'test content';
40+
const testPath = join(testDir, `${randomUUID()}.txt`);
41+
42+
// Create the file
43+
const result = await textEditorTool.execute(
44+
{
45+
command: 'create',
46+
path: testPath,
47+
file_text: testContent,
48+
description: 'test',
49+
},
50+
toolContext,
51+
);
52+
53+
// Verify return value
54+
expect(result.success).toBe(true);
55+
expect(result.message).toContain('File created');
56+
57+
// Verify content
58+
const content = await readFile(testPath, 'utf8');
59+
expect(content).toBe(testContent);
60+
});
61+
62+
it('should view a file', async () => {
63+
const testContent = 'line 1\nline 2\nline 3';
64+
const testPath = join(testDir, `${randomUUID()}.txt`);
65+
66+
// Create the file
67+
await textEditorTool.execute(
68+
{
69+
command: 'create',
70+
path: testPath,
71+
file_text: testContent,
72+
description: 'test',
73+
},
74+
toolContext,
75+
);
76+
77+
// View the file
78+
const result = await textEditorTool.execute(
79+
{
80+
command: 'view',
81+
path: testPath,
82+
description: 'test',
83+
},
84+
toolContext,
85+
);
86+
87+
// Verify return value
88+
expect(result.success).toBe(true);
89+
expect(result.content).toContain('1: line 1');
90+
expect(result.content).toContain('2: line 2');
91+
expect(result.content).toContain('3: line 3');
92+
});
93+
94+
it('should view a file with range', async () => {
95+
const testContent = 'line 1\nline 2\nline 3\nline 4\nline 5';
96+
const testPath = join(testDir, `${randomUUID()}.txt`);
97+
98+
// Create the file
99+
await textEditorTool.execute(
100+
{
101+
command: 'create',
102+
path: testPath,
103+
file_text: testContent,
104+
description: 'test',
105+
},
106+
toolContext,
107+
);
108+
109+
// View the file with range
110+
const result = await textEditorTool.execute(
111+
{
112+
command: 'view',
113+
path: testPath,
114+
view_range: [2, 4],
115+
description: 'test',
116+
},
117+
toolContext,
118+
);
119+
120+
// Verify return value
121+
expect(result.success).toBe(true);
122+
expect(result.content).not.toContain('1: line 1');
123+
expect(result.content).toContain('2: line 2');
124+
expect(result.content).toContain('3: line 3');
125+
expect(result.content).toContain('4: line 4');
126+
expect(result.content).not.toContain('5: line 5');
127+
});
128+
129+
it('should replace text in a file', async () => {
130+
const initialContent = 'Hello world! This is a test.';
131+
const oldStr = 'world';
132+
const newStr = 'universe';
133+
const expectedContent = 'Hello universe! This is a test.';
134+
const testPath = join(testDir, `${randomUUID()}.txt`);
135+
136+
// Create initial file
137+
await textEditorTool.execute(
138+
{
139+
command: 'create',
140+
path: testPath,
141+
file_text: initialContent,
142+
description: 'test',
143+
},
144+
toolContext,
145+
);
146+
147+
// Replace text
148+
const result = await textEditorTool.execute(
149+
{
150+
command: 'str_replace',
151+
path: testPath,
152+
old_str: oldStr,
153+
new_str: newStr,
154+
description: 'test',
155+
},
156+
toolContext,
157+
);
158+
159+
// Verify return value
160+
expect(result.success).toBe(true);
161+
expect(result.message).toContain('Successfully replaced');
162+
163+
// Verify content
164+
const content = await readFile(testPath, 'utf8');
165+
expect(content).toBe(expectedContent);
166+
});
167+
168+
it('should insert text at a specific line', async () => {
169+
const initialContent = 'line 1\nline 2\nline 4';
170+
const insertLine = 2; // After "line 2"
171+
const newStr = 'line 3';
172+
const expectedContent = 'line 1\nline 2\nline 3\nline 4';
173+
const testPath = join(testDir, `${randomUUID()}.txt`);
174+
175+
// Create initial file
176+
await textEditorTool.execute(
177+
{
178+
command: 'create',
179+
path: testPath,
180+
file_text: initialContent,
181+
description: 'test',
182+
},
183+
toolContext,
184+
);
185+
186+
// Insert text
187+
const result = await textEditorTool.execute(
188+
{
189+
command: 'insert',
190+
path: testPath,
191+
insert_line: insertLine,
192+
new_str: newStr,
193+
description: 'test',
194+
},
195+
toolContext,
196+
);
197+
198+
// Verify return value
199+
expect(result.success).toBe(true);
200+
expect(result.message).toContain('Successfully inserted');
201+
202+
// Verify content
203+
const content = await readFile(testPath, 'utf8');
204+
expect(content).toBe(expectedContent);
205+
});
206+
207+
it('should undo an edit', async () => {
208+
const initialContent = 'Hello world!';
209+
const modifiedContent = 'Hello universe!';
210+
const testPath = join(testDir, `${randomUUID()}.txt`);
211+
212+
// Create initial file
213+
await textEditorTool.execute(
214+
{
215+
command: 'create',
216+
path: testPath,
217+
file_text: initialContent,
218+
description: 'test',
219+
},
220+
toolContext,
221+
);
222+
223+
// Modify the file
224+
await textEditorTool.execute(
225+
{
226+
command: 'str_replace',
227+
path: testPath,
228+
old_str: 'world',
229+
new_str: 'universe',
230+
description: 'test',
231+
},
232+
toolContext,
233+
);
234+
235+
// Verify modified content
236+
let content = await readFile(testPath, 'utf8');
237+
expect(content).toBe(modifiedContent);
238+
239+
// Undo the edit
240+
const result = await textEditorTool.execute(
241+
{
242+
command: 'undo_edit',
243+
path: testPath,
244+
description: 'test',
245+
},
246+
toolContext,
247+
);
248+
249+
// Verify return value
250+
expect(result.success).toBe(true);
251+
expect(result.message).toContain('Successfully reverted');
252+
253+
// Verify content is back to initial
254+
content = await readFile(testPath, 'utf8');
255+
expect(content).toBe(initialContent);
256+
});
257+
258+
it('should handle errors for non-existent files', async () => {
259+
const testPath = join(testDir, `${randomUUID()}.txt`);
260+
261+
// Try to view a non-existent file
262+
const result = await textEditorTool.execute(
263+
{
264+
command: 'view',
265+
path: testPath,
266+
description: 'test',
267+
},
268+
toolContext,
269+
);
270+
271+
// Verify return value
272+
expect(result.success).toBe(false);
273+
expect(result.message).toContain('not found');
274+
});
275+
276+
it('should handle errors for duplicate string replacements', async () => {
277+
const initialContent = 'Hello world! This is a world test.';
278+
const oldStr = 'world';
279+
const newStr = 'universe';
280+
const testPath = join(testDir, `${randomUUID()}.txt`);
281+
282+
// Create initial file
283+
await textEditorTool.execute(
284+
{
285+
command: 'create',
286+
path: testPath,
287+
file_text: initialContent,
288+
description: 'test',
289+
},
290+
toolContext,
291+
);
292+
293+
// Try to replace text with multiple occurrences
294+
const result = await textEditorTool.execute(
295+
{
296+
command: 'str_replace',
297+
path: testPath,
298+
old_str: oldStr,
299+
new_str: newStr,
300+
description: 'test',
301+
},
302+
toolContext,
303+
);
304+
305+
// Verify return value
306+
expect(result.success).toBe(false);
307+
expect(result.message).toContain('Found 2 occurrences');
308+
});
309+
});

0 commit comments

Comments
 (0)