Skip to content

Commit dadae25

Browse files
Copilotpelikhan
andcommitted
Implement system.files.edit script with file splice functionality
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent b0ef7fe commit dadae25

File tree

4 files changed

+378
-0
lines changed

4 files changed

+378
-0
lines changed

docs/src/components/BuiltinTools.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LinkCard } from '@astrojs/starlight/components';
77
### Builtin tools
88

99
<LinkCard title="fetch" description="Fetch data from a URL from allowed domains." href="/genaiscript/reference/scripts/system#systemfetch" />
10+
<LinkCard title="files_edit" description="Edits a file by splicing lines at a specific position. Similar to JavaScript's Array.splice() but for file lines. Removes a specified number of lines starting from a given line number and inserts new lines at that position." href="/genaiscript/reference/scripts/system#systemfilesedit" />
1011
<LinkCard title="fs_ask_file" description="Runs a LLM query over the content of a file. Use this tool to extract information from a file." href="/genaiscript/reference/scripts/system#systemfs_ask_file" />
1112
<LinkCard title="fs_data_query" description="Query data in a file using GROQ syntax" href="/genaiscript/reference/scripts/system#systemfs_data_query" />
1213
<LinkCard title="fs_diff_files" description="Computes a diff between two different files. Use git diff instead to compare versions of a file." href="/genaiscript/reference/scripts/system#systemfs_diff_files" />

docs/src/content/docs/reference/scripts/system.mdx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,135 @@ Replace line range 30-35 in \n${folder}/file1.py
14201420
`````
14211421
14221422
1423+
### `system.files.edit`
1424+
1425+
File Edit
1426+
1427+
Function to edit a file by splicing lines at a specific position.
1428+
1429+
- tool `files_edit`: Edits a file by splicing lines at a specific position. Similar to JavaScript's Array.splice() but for file lines. Removes a specified number of lines starting from a given line number and inserts new lines at that position.
1430+
1431+
`````js wrap title="system.files.edit"
1432+
system({
1433+
title: "File Edit",
1434+
description: "Function to edit a file by splicing lines at a specific position.",
1435+
})
1436+
1437+
export default function (ctx: ChatGenerationContext) {
1438+
const { defTool } = ctx
1439+
1440+
defTool(
1441+
"files_edit",
1442+
"Edits a file by splicing lines at a specific position. Similar to JavaScript's Array.splice() but for file lines. Removes a specified number of lines starting from a given line number and inserts new lines at that position.",
1443+
{
1444+
type: "object",
1445+
properties: {
1446+
filename: {
1447+
type: "string",
1448+
description:
1449+
"Path of the file to edit, relative to the workspace root. Must be within the workspace boundary.",
1450+
},
1451+
insertLine: {
1452+
type: "integer",
1453+
description:
1454+
"Line number (1-based) where to start the edit. Lines will be inserted at this position after removing deleteCount lines.",
1455+
minimum: 1,
1456+
},
1457+
deleteCount: {
1458+
type: "integer",
1459+
description:
1460+
"Number of lines to delete starting from insertLine. Use 0 to insert without deleting.",
1461+
minimum: 0,
1462+
default: 0,
1463+
},
1464+
lines: {
1465+
type: "array",
1466+
items: {
1467+
type: "string",
1468+
},
1469+
description:
1470+
"Array of lines to insert at the specified position. Use empty array to only delete lines.",
1471+
default: [],
1472+
},
1473+
},
1474+
required: ["filename", "insertLine"],
1475+
},
1476+
async (args) => {
1477+
const { filename, insertLine, deleteCount = 0, lines = [], context } = args
1478+
1479+
if (!filename) return "<MISSING>filename</MISSING>"
1480+
if (insertLine < 1) return "<ERROR>insertLine must be >= 1</ERROR>"
1481+
if (deleteCount < 0) return "<ERROR>deleteCount must be >= 0</ERROR>"
1482+
1483+
try {
1484+
context.log(`edit ${filename} at line ${insertLine}: delete ${deleteCount}, insert ${lines.length} lines`)
1485+
1486+
// Read the current file content
1487+
let fileContent: string
1488+
try {
1489+
const result = await workspace.readText(filename)
1490+
fileContent = result.content ?? ""
1491+
} catch (e) {
1492+
// If file doesn't exist and we're only inserting, create it
1493+
if (deleteCount === 0) {
1494+
fileContent = ""
1495+
} else {
1496+
return `<ERROR>File not found: ${filename}</ERROR>`
1497+
}
1498+
}
1499+
1500+
// Split content into lines
1501+
const fileLines = fileContent.split(/\r?\n/)
1502+
1503+
// Validate insertLine is within valid range for the file
1504+
if (insertLine > fileLines.length + 1) {
1505+
return `<ERROR>insertLine ${insertLine} is beyond file length (${fileLines.length} lines)</ERROR>`
1506+
}
1507+
1508+
// Convert to 0-based index for splice operation
1509+
const zeroBasedIndex = insertLine - 1
1510+
1511+
// Validate deleteCount doesn't exceed available lines
1512+
const availableLines = fileLines.length - zeroBasedIndex
1513+
if (deleteCount > availableLines) {
1514+
return `<ERROR>deleteCount ${deleteCount} exceeds available lines ${availableLines} from line ${insertLine}</ERROR>`
1515+
}
1516+
1517+
// Perform the splice operation
1518+
const deletedLines = fileLines.splice(zeroBasedIndex, deleteCount, ...lines)
1519+
1520+
// Join the lines back into file content
1521+
const newContent = fileLines.join("\n")
1522+
1523+
// Write the updated content back to the file
1524+
await workspace.writeText(filename, newContent)
1525+
1526+
// Log the operation details
1527+
const operation = []
1528+
if (deleteCount > 0) {
1529+
operation.push(`deleted ${deleteCount} line${deleteCount === 1 ? '' : 's'}`)
1530+
}
1531+
if (lines.length > 0) {
1532+
operation.push(`inserted ${lines.length} line${lines.length === 1 ? '' : 's'}`)
1533+
}
1534+
1535+
const summary = operation.length > 0 ? operation.join(', ') : 'no changes'
1536+
return `File ${filename} edited successfully at line ${insertLine}: ${summary}`
1537+
1538+
} catch (e) {
1539+
const error = e instanceof Error ? e.message : String(e)
1540+
context.log(`Error editing ${filename}: ${error}`)
1541+
return `<ERROR>Failed to edit file: ${error}</ERROR>`
1542+
}
1543+
},
1544+
{
1545+
maxTokens: 1000,
1546+
}
1547+
)
1548+
}
1549+
`````
1550+
1551+
14231552
### `system.files_schema`
14241553
14251554
Apply JSON schemas to generated data.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
system({
2+
title: "File Edit",
3+
description: "Function to edit a file by splicing lines at a specific position.",
4+
})
5+
6+
export default function (ctx: ChatGenerationContext) {
7+
const { defTool } = ctx
8+
9+
defTool(
10+
"files_edit",
11+
"Edits a file by splicing lines at a specific position. Similar to JavaScript's Array.splice() but for file lines. Removes a specified number of lines starting from a given line number and inserts new lines at that position.",
12+
{
13+
type: "object",
14+
properties: {
15+
filename: {
16+
type: "string",
17+
description:
18+
"Path of the file to edit, relative to the workspace root. Must be within the workspace boundary.",
19+
},
20+
insertLine: {
21+
type: "integer",
22+
description:
23+
"Line number (1-based) where to start the edit. Lines will be inserted at this position after removing deleteCount lines.",
24+
minimum: 1,
25+
},
26+
deleteCount: {
27+
type: "integer",
28+
description:
29+
"Number of lines to delete starting from insertLine. Use 0 to insert without deleting.",
30+
minimum: 0,
31+
default: 0,
32+
},
33+
lines: {
34+
type: "array",
35+
items: {
36+
type: "string",
37+
},
38+
description:
39+
"Array of lines to insert at the specified position. Use empty array to only delete lines.",
40+
default: [],
41+
},
42+
},
43+
required: ["filename", "insertLine"],
44+
},
45+
async (args) => {
46+
const { filename, insertLine, deleteCount = 0, lines = [], context } = args
47+
48+
if (!filename) return "<MISSING>filename</MISSING>"
49+
if (insertLine < 1) return "<ERROR>insertLine must be >= 1</ERROR>"
50+
if (deleteCount < 0) return "<ERROR>deleteCount must be >= 0</ERROR>"
51+
52+
try {
53+
context.log(`edit ${filename} at line ${insertLine}: delete ${deleteCount}, insert ${lines.length} lines`)
54+
55+
// Read the current file content
56+
let fileContent: string
57+
try {
58+
const result = await workspace.readText(filename)
59+
fileContent = result.content ?? ""
60+
} catch (e) {
61+
// If file doesn't exist and we're only inserting, create it
62+
if (deleteCount === 0) {
63+
fileContent = ""
64+
} else {
65+
return `<ERROR>File not found: ${filename}</ERROR>`
66+
}
67+
}
68+
69+
// Split content into lines
70+
const fileLines = fileContent.split(/\r?\n/)
71+
72+
// Validate insertLine is within valid range for the file
73+
if (insertLine > fileLines.length + 1) {
74+
return `<ERROR>insertLine ${insertLine} is beyond file length (${fileLines.length} lines)</ERROR>`
75+
}
76+
77+
// Convert to 0-based index for splice operation
78+
const zeroBasedIndex = insertLine - 1
79+
80+
// Validate deleteCount doesn't exceed available lines
81+
const availableLines = fileLines.length - zeroBasedIndex
82+
if (deleteCount > availableLines) {
83+
return `<ERROR>deleteCount ${deleteCount} exceeds available lines ${availableLines} from line ${insertLine}</ERROR>`
84+
}
85+
86+
// Perform the splice operation
87+
const deletedLines = fileLines.splice(zeroBasedIndex, deleteCount, ...lines)
88+
89+
// Join the lines back into file content
90+
const newContent = fileLines.join("\n")
91+
92+
// Write the updated content back to the file
93+
await workspace.writeText(filename, newContent)
94+
95+
// Log the operation details
96+
const operation = []
97+
if (deleteCount > 0) {
98+
operation.push(`deleted ${deleteCount} line${deleteCount === 1 ? '' : 's'}`)
99+
}
100+
if (lines.length > 0) {
101+
operation.push(`inserted ${lines.length} line${lines.length === 1 ? '' : 's'}`)
102+
}
103+
104+
const summary = operation.length > 0 ? operation.join(', ') : 'no changes'
105+
return `File ${filename} edited successfully at line ${insertLine}: ${summary}`
106+
107+
} catch (e) {
108+
const error = e instanceof Error ? e.message : String(e)
109+
context.log(`Error editing ${filename}: ${error}`)
110+
return `<ERROR>Failed to edit file: ${error}</ERROR>`
111+
}
112+
},
113+
{
114+
maxTokens: 1000,
115+
}
116+
)
117+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest"
2+
import { readFile, writeFile, unlink } from "fs/promises"
3+
import { join } from "path"
4+
import { tmpdir } from "os"
5+
6+
// Mock workspace for testing
7+
const mockWorkspace = {
8+
async readText(filename: string) {
9+
try {
10+
const content = await readFile(filename, "utf-8")
11+
return { content }
12+
} catch (e) {
13+
throw new Error(`File not found: ${filename}`)
14+
}
15+
},
16+
async writeText(filename: string, content: string) {
17+
await writeFile(filename, content, "utf-8")
18+
}
19+
}
20+
21+
// Mock context
22+
const mockContext = {
23+
log: (message: string) => console.log(message)
24+
}
25+
26+
describe("system.files.edit", () => {
27+
let testFile: string
28+
let originalContent: string
29+
30+
beforeEach(async () => {
31+
testFile = join(tmpdir(), `test-file-${Date.now()}.txt`)
32+
originalContent = "line 1\nline 2\nline 3\nline 4\nline 5"
33+
await writeFile(testFile, originalContent, "utf-8")
34+
})
35+
36+
afterEach(async () => {
37+
try {
38+
await unlink(testFile)
39+
} catch {
40+
// Ignore if file doesn't exist
41+
}
42+
})
43+
44+
it("should insert lines without deleting", async () => {
45+
// Mock the tool function (simplified for testing)
46+
const editFile = async (args: any) => {
47+
const { filename, insertLine, deleteCount = 0, lines = [] } = args
48+
49+
const result = await mockWorkspace.readText(filename)
50+
const fileContent = result.content ?? ""
51+
const fileLines = fileContent.split(/\r?\n/)
52+
53+
const zeroBasedIndex = insertLine - 1
54+
fileLines.splice(zeroBasedIndex, deleteCount, ...lines)
55+
56+
const newContent = fileLines.join("\n")
57+
await mockWorkspace.writeText(filename, newContent)
58+
59+
return `File edited successfully`
60+
}
61+
62+
await editFile({
63+
filename: testFile,
64+
insertLine: 3,
65+
deleteCount: 0,
66+
lines: ["inserted line A", "inserted line B"]
67+
})
68+
69+
const result = await readFile(testFile, "utf-8")
70+
const expected = "line 1\nline 2\ninserted line A\ninserted line B\nline 3\nline 4\nline 5"
71+
expect(result).toBe(expected)
72+
})
73+
74+
it("should delete lines without inserting", async () => {
75+
const editFile = async (args: any) => {
76+
const { filename, insertLine, deleteCount = 0, lines = [] } = args
77+
78+
const result = await mockWorkspace.readText(filename)
79+
const fileContent = result.content ?? ""
80+
const fileLines = fileContent.split(/\r?\n/)
81+
82+
const zeroBasedIndex = insertLine - 1
83+
fileLines.splice(zeroBasedIndex, deleteCount, ...lines)
84+
85+
const newContent = fileLines.join("\n")
86+
await mockWorkspace.writeText(filename, newContent)
87+
88+
return `File edited successfully`
89+
}
90+
91+
await editFile({
92+
filename: testFile,
93+
insertLine: 2,
94+
deleteCount: 2,
95+
lines: []
96+
})
97+
98+
const result = await readFile(testFile, "utf-8")
99+
const expected = "line 1\nline 4\nline 5"
100+
expect(result).toBe(expected)
101+
})
102+
103+
it("should replace lines (delete and insert)", async () => {
104+
const editFile = async (args: any) => {
105+
const { filename, insertLine, deleteCount = 0, lines = [] } = args
106+
107+
const result = await mockWorkspace.readText(filename)
108+
const fileContent = result.content ?? ""
109+
const fileLines = fileContent.split(/\r?\n/)
110+
111+
const zeroBasedIndex = insertLine - 1
112+
fileLines.splice(zeroBasedIndex, deleteCount, ...lines)
113+
114+
const newContent = fileLines.join("\n")
115+
await mockWorkspace.writeText(filename, newContent)
116+
117+
return `File edited successfully`
118+
}
119+
120+
await editFile({
121+
filename: testFile,
122+
insertLine: 2,
123+
deleteCount: 2,
124+
lines: ["replacement line 1", "replacement line 2", "replacement line 3"]
125+
})
126+
127+
const result = await readFile(testFile, "utf-8")
128+
const expected = "line 1\nreplacement line 1\nreplacement line 2\nreplacement line 3\nline 4\nline 5"
129+
expect(result).toBe(expected)
130+
})
131+
})

0 commit comments

Comments
 (0)