Skip to content

Commit 3a33bf0

Browse files
committed
feat: add createDir and editFile tool
1 parent c047ada commit 3a33bf0

File tree

3 files changed

+385
-0
lines changed

3 files changed

+385
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import fsOperation from "fileSystem";
2+
import { StructuredTool } from "@langchain/core/tools";
3+
import { addedFolder } from "lib/openFolder";
4+
import { z } from "zod";
5+
6+
/**
7+
* Tool for creating directories in Acode
8+
*/
9+
class CreateDirTool extends StructuredTool {
10+
name = "createDir";
11+
description =
12+
"Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.";
13+
schema = z.object({
14+
path: z
15+
.string()
16+
.describe(
17+
"The relative path of the directory to create. This path should never be absolute, and the first component of the path should always be a root directory in a project (opened in sidebar). For example, if root directories are 'directory1' and 'directory2', to create 'newdir' in 'directory1', use 'directory1/newdir'. To create nested directories like 'directory1/foo/bar', use 'directory1/foo/bar'.",
18+
),
19+
});
20+
21+
/**
22+
* Check if a URI is a SAF URI
23+
*/
24+
isSafUri(uri) {
25+
return uri.startsWith("content://") && uri.includes("/tree/");
26+
}
27+
28+
/**
29+
* Check if SAF URI already has the :: separator (complete format)
30+
*/
31+
isCompleteSafUri(uri) {
32+
return this.isSafUri(uri) && uri.includes("::");
33+
}
34+
35+
/**
36+
* Construct SAF URI for directory access
37+
*/
38+
constructSafDirUri(baseUri, dirPath) {
39+
// For incomplete SAF URIs (without ::), construct the full format
40+
// baseUri::primary:fullDirPath
41+
return `${baseUri}::primary:${dirPath}`;
42+
}
43+
44+
/**
45+
* Check if directory exists
46+
*/
47+
async directoryExists(dirUrl) {
48+
try {
49+
const stat = await fsOperation(dirUrl).stat();
50+
return stat.isDirectory;
51+
} catch (error) {
52+
return false;
53+
}
54+
}
55+
56+
async _call({ path }) {
57+
try {
58+
// Split the path to get project name and directory path
59+
const pathParts = path.split("/");
60+
const projectName = pathParts[0];
61+
const dirPath = pathParts.slice(1).join("/");
62+
63+
// Find the project in addedFolder array
64+
const project = addedFolder.find(
65+
(folder) => folder.title === projectName,
66+
);
67+
if (!project) {
68+
return `Error: Project '${projectName}' not found in opened projects`;
69+
}
70+
71+
let dirUrl;
72+
73+
// Check if this is a SAF URI
74+
if (this.isSafUri(project.url)) {
75+
if (this.isCompleteSafUri(project.url)) {
76+
// SAF URI already has :: separator, just append the directory path normally
77+
// Handle both cases: with trailing slash or without
78+
const baseUrl = project.url.endsWith("/")
79+
? project.url
80+
: project.url + "/";
81+
dirUrl = baseUrl + dirPath;
82+
} else {
83+
// SAF URI without :: separator, use the special format
84+
dirUrl = this.constructSafDirUri(project.url, path);
85+
}
86+
} else {
87+
// For regular file URIs, use the normal path concatenation
88+
dirUrl = project.url + "/" + dirPath;
89+
}
90+
91+
// Check if directory already exists
92+
const exists = await this.directoryExists(dirUrl);
93+
if (exists) {
94+
return `Directory '${path}' already exists.`;
95+
}
96+
97+
// Create the directory using createDirectory method
98+
// Extract parent directory URL and directory name
99+
const dirName = dirPath.split("/").pop();
100+
const parentPath = pathParts.slice(1, -1).join("/");
101+
102+
let parentUrl;
103+
if (this.isSafUri(project.url)) {
104+
if (this.isCompleteSafUri(project.url)) {
105+
const baseUrl = project.url.endsWith("/")
106+
? project.url
107+
: project.url + "/";
108+
parentUrl = parentPath ? baseUrl + parentPath : project.url;
109+
} else {
110+
parentUrl = parentPath
111+
? this.constructSafDirUri(
112+
project.url,
113+
projectName + "/" + parentPath,
114+
)
115+
: project.url;
116+
}
117+
} else {
118+
parentUrl = parentPath ? project.url + "/" + parentPath : project.url;
119+
}
120+
121+
await fsOperation(parentUrl).createDirectory(dirName);
122+
123+
return `Directory '${path}' has been successfully created.`;
124+
} catch (error) {
125+
return `Error creating directory: ${error.message}`;
126+
}
127+
}
128+
}
129+
130+
export const createDir = new CreateDirTool();
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import fsOperation from "fileSystem";
2+
import { StructuredTool } from "@langchain/core/tools";
3+
import { addedFolder } from "lib/openFolder";
4+
import { z } from "zod";
5+
6+
/**
7+
* Tool for creating a new file or editing an existing file in Acode
8+
*/
9+
class EditFileTool extends StructuredTool {
10+
name = "editFile";
11+
description =
12+
"This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.";
13+
schema = z.object({
14+
path: z
15+
.string()
16+
.describe(
17+
"The relative path of the file to edit or create. This path should never be absolute, and the first component of the path should always be a root directory in a project (opened in sidebar). For example, if root directories are 'directory1' and 'directory2', to edit 'file.txt' in 'directory1', use 'directory1/file.txt'. To edit 'file.txt' in 'directory2', use 'directory2/file.txt'.",
18+
),
19+
mode: z
20+
.enum(["edit", "create", "overwrite"])
21+
.describe(
22+
"The mode of operation on the file. Possible values: 'edit' - Make granular edits to an existing file (requires oldString and newString), 'create' - Create a new file if it doesn't exist, 'overwrite' - Replace the entire contents of an existing file. When a file already exists, prefer editing it as opposed to recreating it from scratch.",
23+
),
24+
content: z
25+
.string()
26+
.optional()
27+
.describe(
28+
"The content to write to the file. Required for 'create' and 'overwrite' modes.",
29+
),
30+
oldString: z
31+
.string()
32+
.optional()
33+
.describe(
34+
"The text to replace (required for 'edit' mode). Must match exactly including whitespace. Can be empty string to insert text at the beginning of newString location.",
35+
),
36+
newString: z
37+
.string()
38+
.optional()
39+
.describe(
40+
"The replacement text (required for 'edit' mode). Can be empty string to delete the oldString.",
41+
),
42+
replaceAll: z
43+
.boolean()
44+
.optional()
45+
.describe(
46+
"If true, replace all occurrences of oldString. If false (default), replace only the first occurrence.",
47+
),
48+
});
49+
50+
/**
51+
* Check if a URI is a SAF URI
52+
*/
53+
isSafUri(uri) {
54+
return uri.startsWith("content://") && uri.includes("/tree/");
55+
}
56+
57+
/**
58+
* Check if SAF URI already has the :: separator (complete format)
59+
*/
60+
isCompleteSafUri(uri) {
61+
return this.isSafUri(uri) && uri.includes("::");
62+
}
63+
64+
/**
65+
* Construct SAF URI for file access
66+
*/
67+
constructSafFileUri(baseUri, filePath) {
68+
// For incomplete SAF URIs (without ::), construct the full format
69+
// baseUri::primary:fullFilePath
70+
return `${baseUri}::primary:${filePath}`;
71+
}
72+
73+
/**
74+
* Check if file exists
75+
*/
76+
async fileExists(fileUrl) {
77+
try {
78+
const stat = await fsOperation(fileUrl).stat();
79+
return stat.isFile;
80+
} catch (error) {
81+
return false;
82+
}
83+
}
84+
85+
async _call({
86+
path,
87+
mode,
88+
content,
89+
oldString,
90+
newString,
91+
replaceAll = false,
92+
}) {
93+
try {
94+
// Validate inputs based on mode
95+
if (mode === "edit") {
96+
if (oldString === undefined || newString === undefined) {
97+
return `Error: 'edit' mode requires both 'oldString' and 'newString' parameters.`;
98+
}
99+
} else if (mode === "create" || mode === "overwrite") {
100+
if (content === undefined) {
101+
return `Error: '${mode}' mode requires 'content' parameter.`;
102+
}
103+
}
104+
105+
// Split the path to get project name and file path
106+
const pathParts = path.split("/");
107+
const projectName = pathParts[0];
108+
const filePath = pathParts.slice(1).join("/");
109+
110+
// Find the project in addedFolder array
111+
const project = addedFolder.find(
112+
(folder) => folder.title === projectName,
113+
);
114+
if (!project) {
115+
return `Error: Project '${projectName}' not found in opened projects`;
116+
}
117+
118+
let fileUrl;
119+
120+
// Check if this is a SAF URI
121+
if (this.isSafUri(project.url)) {
122+
if (this.isCompleteSafUri(project.url)) {
123+
// SAF URI already has :: separator, just append the file path normally
124+
// Handle both cases: with trailing slash or without
125+
const baseUrl = project.url.endsWith("/")
126+
? project.url
127+
: project.url + "/";
128+
fileUrl = baseUrl + filePath;
129+
} else {
130+
// SAF URI without :: separator, use the special format
131+
fileUrl = this.constructSafFileUri(project.url, path);
132+
}
133+
} else {
134+
// For regular file URIs, use the normal path concatenation
135+
fileUrl = project.url + "/" + filePath;
136+
}
137+
138+
// Check if file exists
139+
const exists = await this.fileExists(fileUrl);
140+
141+
// Handle different modes
142+
switch (mode) {
143+
case "create":
144+
if (exists) {
145+
return `Error: File '${path}' already exists. Use 'edit' or 'overwrite' mode instead.`;
146+
}
147+
// For creating files, we need to use createFile method
148+
// Extract directory URL and filename
149+
const fileName = filePath.split("/").pop();
150+
const dirPath = pathParts.slice(1, -1).join("/");
151+
152+
let dirUrl;
153+
if (this.isSafUri(project.url)) {
154+
if (this.isCompleteSafUri(project.url)) {
155+
const baseUrl = project.url.endsWith("/")
156+
? project.url
157+
: project.url + "/";
158+
dirUrl = dirPath ? baseUrl + dirPath : project.url;
159+
} else {
160+
dirUrl = dirPath
161+
? this.constructSafFileUri(
162+
project.url,
163+
projectName + "/" + dirPath,
164+
)
165+
: project.url;
166+
}
167+
} else {
168+
dirUrl = dirPath ? project.url + "/" + dirPath : project.url;
169+
}
170+
171+
await fsOperation(dirUrl).createFile(fileName, content);
172+
return `File '${path}' has been successfully created.`;
173+
174+
case "overwrite":
175+
if (!exists) {
176+
return `Error: File '${path}' does not exist. Use 'create' mode instead.`;
177+
}
178+
await fsOperation(fileUrl).writeFile(content);
179+
return `File '${path}' has been successfully overwritten.`;
180+
181+
case "edit":
182+
if (!exists) {
183+
return `Error: File '${path}' does not exist. Use 'create' mode instead.`;
184+
}
185+
186+
// Read current content
187+
const currentContent = await fsOperation(fileUrl).readFile("utf8");
188+
189+
// Handle empty oldString (insertion at beginning of file)
190+
if (oldString === "") {
191+
const updatedContent = newString + currentContent;
192+
await fsOperation(fileUrl).writeFile(updatedContent);
193+
return `File '${path}' has been successfully edited. Inserted text at beginning of file.`;
194+
}
195+
196+
// Check if oldString exists in the file
197+
if (!currentContent.includes(oldString)) {
198+
// Provide more helpful error message
199+
const lines = currentContent.split("\n");
200+
const preview =
201+
lines.length > 5
202+
? `First 5 lines:\n${lines
203+
.slice(0, 5)
204+
.map((line, i) => `${i + 1}: ${line}`)
205+
.join("\n")}`
206+
: `File content:\n${lines.map((line, i) => `${i + 1}: ${line}`).join("\n")}`;
207+
return `Error: The text to replace was not found in '${path}'.\n\nSearching for:\n${JSON.stringify(oldString)}\n\n${preview}`;
208+
}
209+
210+
// Count occurrences for reporting
211+
const occurrenceCount = (
212+
currentContent.match(
213+
new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
214+
) || []
215+
).length;
216+
217+
// Perform the replacement
218+
let updatedContent;
219+
if (replaceAll) {
220+
// Replace all occurrences using replaceAll method
221+
updatedContent = currentContent.replaceAll(oldString, newString);
222+
} else {
223+
// Replace only first occurrence
224+
updatedContent = currentContent.replace(oldString, newString);
225+
}
226+
227+
// Check if replacement actually changed the content
228+
if (updatedContent === currentContent) {
229+
return `Warning: No changes were made to '${path}'. The 'oldString' and 'newString' are identical.`;
230+
}
231+
232+
// Write the updated content
233+
await fsOperation(fileUrl).writeFile(updatedContent);
234+
235+
const replacedCount = replaceAll ? occurrenceCount : 1;
236+
const message = replaceAll
237+
? `File '${path}' has been successfully edited. Replaced ${replacedCount} occurrence(s) of the text.`
238+
: `File '${path}' has been successfully edited. Replaced first occurrence of the text (${occurrenceCount} total found).`;
239+
240+
return message;
241+
242+
default:
243+
return `Error: Invalid mode '${mode}'. Use 'create', 'edit', or 'overwrite'.`;
244+
}
245+
} catch (error) {
246+
return `Error processing file: ${error.message}`;
247+
}
248+
}
249+
}
250+
251+
export const editFile = new EditFileTool();

src/pages/aiAssistant/tools/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createDir } from "./createDir";
2+
import { editFile } from "./editFile";
13
import { fetchTool } from "./fetch";
24
import { listDirectory } from "./listDirectory";
35
import { readFile } from "./readFile";
@@ -7,4 +9,6 @@ export const allTools = {
79
readFile,
810
fetchTool,
911
listDirectory,
12+
editFile,
13+
createDir,
1014
};

0 commit comments

Comments
 (0)