Skip to content

Commit 73098b7

Browse files
committed
🤖 fix: allow file_edit_insert to create files
Generated with
1 parent 6a51d58 commit 73098b7

File tree

4 files changed

+69
-8
lines changed

4 files changed

+69
-8
lines changed

src/services/tools/file_edit_insert.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ describe("file_edit_insert tool", () => {
9696
}
9797
});
9898

99+
it("creates new file when create flag is provided", async () => {
100+
const newFile = path.join(testDir, "new.txt");
101+
const tool = createTestTool(testDir);
102+
const args: FileEditInsertToolArgs = {
103+
file_path: path.relative(testDir, newFile),
104+
content: "Hello world!\n",
105+
create: true,
106+
};
107+
108+
const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditInsertToolResult;
109+
expect(result.success).toBe(true);
110+
expect(await fs.readFile(newFile, "utf-8")).toBe("Hello world!\n");
111+
});
112+
99113
it("fails when no guards are provided", async () => {
100114
const tool = createTestTool(testDir);
101115
const args: FileEditInsertToolArgs = {

src/services/tools/file_edit_insert.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import type { FileEditInsertToolArgs, FileEditInsertToolResult } from "@/types/t
33
import { EDIT_FAILED_NOTE_PREFIX, NOTE_READ_FILE_RETRY } from "@/types/tools";
44
import type { ToolConfiguration, ToolFactory } from "@/utils/tools/tools";
55
import { TOOL_DEFINITIONS } from "@/utils/tools/toolDefinitions";
6-
import { validateAndCorrectPath, validatePathInCwd } from "./fileCommon";
6+
import { generateDiff, validateAndCorrectPath, validatePathInCwd } from "./fileCommon";
77
import { executeFileEditOperation } from "./file_edit_operation";
8+
import { fileExists } from "@/utils/runtime/fileExists";
9+
import { writeFileString } from "@/utils/runtime/helpers";
10+
import { RuntimeError } from "@/runtime/Runtime";
811

912
const READ_AND_RETRY_NOTE = `${EDIT_FAILED_NOTE_PREFIX} ${NOTE_READ_FILE_RETRY}`;
1013

@@ -45,11 +48,15 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
4548
description: TOOL_DEFINITIONS.file_edit_insert.description,
4649
inputSchema: TOOL_DEFINITIONS.file_edit_insert.schema,
4750
execute: async (
48-
{ file_path, content, before, after }: FileEditInsertToolArgs,
51+
{ file_path, content, before, after, create }: FileEditInsertToolArgs,
4952
{ abortSignal }
5053
): Promise<FileEditInsertToolResult> => {
5154
try {
52-
const { correctedPath } = validateAndCorrectPath(file_path, config.cwd, config.runtime);
55+
const { correctedPath, warning: pathWarning } = validateAndCorrectPath(
56+
file_path,
57+
config.cwd,
58+
config.runtime
59+
);
5360
file_path = correctedPath;
5461

5562
const pathValidation = validatePathInCwd(file_path, config.cwd, config.runtime);
@@ -60,6 +67,38 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration)
6067
};
6168
}
6269

70+
const resolvedPath = config.runtime.normalizePath(file_path, config.cwd);
71+
const exists = await fileExists(config.runtime, resolvedPath, abortSignal);
72+
73+
if (!exists) {
74+
if (!create) {
75+
return {
76+
success: false,
77+
error: `File not found: ${file_path}. Set create: true to create it.`,
78+
note: `${EDIT_FAILED_NOTE_PREFIX} File does not exist. Set create: true to create it, or check the file path.`,
79+
};
80+
}
81+
82+
try {
83+
await writeFileString(config.runtime, resolvedPath, content, abortSignal);
84+
} catch (err) {
85+
if (err instanceof RuntimeError) {
86+
return {
87+
success: false,
88+
error: err.message,
89+
};
90+
}
91+
throw err;
92+
}
93+
94+
const diff = generateDiff(resolvedPath, "", content);
95+
return {
96+
success: true,
97+
diff,
98+
...(pathWarning && { warning: pathWarning }),
99+
};
100+
}
101+
63102
return executeFileEditOperation({
64103
config,
65104
filePath: file_path,

src/types/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export interface FileEditErrorResult {
7474
export interface FileEditInsertToolArgs {
7575
file_path: string;
7676
content: string;
77+
/** When true, create the file if it doesn't exist */
78+
create?: boolean;
7779
/** Optional substring that must appear immediately before the insertion point */
7880
before?: string;
7981
/** Optional substring that must appear immediately after the insertion point */

src/utils/tools/toolDefinitions.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,14 @@ export const TOOL_DEFINITIONS = {
115115
file_edit_insert: {
116116
description:
117117
"Insert content into a file using substring guards. " +
118-
"Provide exactly one of before or after to anchor the operation. " +
118+
"Provide exactly one of before or after to anchor the operation when editing an existing file. " +
119+
"Set create: true to write a brand new file without guards. " +
119120
`Optional before/after substrings must uniquely match surrounding content. ${TOOL_EDIT_WARNING}`,
120121
schema: z
121122
.object({
122123
file_path: FILE_EDIT_FILE_PATH,
123124
content: z.string().describe("The content to insert"),
125+
create: z.boolean().optional().describe("If true, create the file if it does not exist"),
124126
before: z
125127
.string()
126128
.min(1)
@@ -132,10 +134,14 @@ export const TOOL_DEFINITIONS = {
132134
.optional()
133135
.describe("Optional substring that must appear immediately after the insertion point"),
134136
})
135-
.refine((data) => data.before !== undefined || data.after !== undefined, {
136-
message: "Provide either before or after to anchor the insertion point.",
137-
path: ["before"],
138-
})
137+
.refine(
138+
(data) => data.create === true || data.before !== undefined || data.after !== undefined,
139+
{
140+
message:
141+
"Provide before or after when editing existing files, or set create: true to write a new file.",
142+
path: ["before"],
143+
}
144+
)
139145
.refine((data) => !(data.before !== undefined && data.after !== undefined), {
140146
message: "Provide only one of before or after (not both).",
141147
path: ["before"],

0 commit comments

Comments
 (0)