@ottocode/sdk 0.1.236 → 0.1.237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/src/tools/builtin/fs/edit-shared.ts +83 -0
- package/src/core/src/tools/builtin/fs/edit.ts +129 -0
- package/src/core/src/tools/builtin/fs/edit.txt +9 -0
- package/src/core/src/tools/builtin/fs/index.ts +4 -0
- package/src/core/src/tools/builtin/fs/multiedit.ts +137 -0
- package/src/core/src/tools/builtin/fs/multiedit.txt +9 -0
- package/src/core/src/tools/builtin/fs/read-tracker.ts +72 -0
- package/src/core/src/tools/builtin/fs/read.ts +2 -0
- package/src/core/src/tools/builtin/fs/write.ts +2 -0
- package/src/core/src/tools/builtin/fs/write.txt +1 -1
- package/src/prompts/src/agents/build.txt +9 -2
- package/src/prompts/src/base.txt +10 -7
- package/src/prompts/src/providers/default.txt +25 -14
- package/src/prompts/src/providers/glm.txt +25 -14
- package/src/prompts/src/providers/google.txt +3 -3
- package/src/prompts/src/providers/moonshot.txt +25 -14
- package/src/prompts/src/providers/openai.txt +1 -1
- package/src/providers/src/catalog.ts +497 -7
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export function normalizeLineEndings(text: string): string {
|
|
2
|
+
return text.replace(/\r\n/g, '\n');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function detectLineEnding(text: string): '\n' | '\r\n' {
|
|
6
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function convertToLineEnding(
|
|
10
|
+
text: string,
|
|
11
|
+
lineEnding: '\n' | '\r\n',
|
|
12
|
+
): string {
|
|
13
|
+
if (lineEnding === '\n') return text;
|
|
14
|
+
return text.replace(/\n/g, '\r\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function countOccurrences(content: string, search: string): number {
|
|
18
|
+
if (!search) return 0;
|
|
19
|
+
let count = 0;
|
|
20
|
+
let start = 0;
|
|
21
|
+
while (true) {
|
|
22
|
+
const index = content.indexOf(search, start);
|
|
23
|
+
if (index === -1) return count;
|
|
24
|
+
count += 1;
|
|
25
|
+
start = index + search.length;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyStringEdit(
|
|
30
|
+
content: string,
|
|
31
|
+
oldString: string,
|
|
32
|
+
newString: string,
|
|
33
|
+
replaceAll = false,
|
|
34
|
+
): { content: string; occurrences: number } {
|
|
35
|
+
if (oldString.length === 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'oldString must not be empty. Use write to create files or apply_patch for structural insertions.',
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (oldString === newString) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'No changes to apply: oldString and newString are identical.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lineEnding = detectLineEnding(content);
|
|
47
|
+
const normalizedOld = convertToLineEnding(
|
|
48
|
+
normalizeLineEndings(oldString),
|
|
49
|
+
lineEnding,
|
|
50
|
+
);
|
|
51
|
+
const normalizedNew = convertToLineEnding(
|
|
52
|
+
normalizeLineEndings(newString),
|
|
53
|
+
lineEnding,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const occurrences = countOccurrences(content, normalizedOld);
|
|
57
|
+
if (occurrences === 0) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'oldString not found in content. Read the file again and copy the exact text, including whitespace.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (occurrences > 1 && !replaceAll) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'Found multiple matches for oldString. Provide more surrounding lines to make it unique or set replaceAll to true.',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (replaceAll) {
|
|
69
|
+
return {
|
|
70
|
+
content: content.split(normalizedOld).join(normalizedNew),
|
|
71
|
+
occurrences,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const index = content.indexOf(normalizedOld);
|
|
76
|
+
return {
|
|
77
|
+
content:
|
|
78
|
+
content.slice(0, index) +
|
|
79
|
+
normalizedNew +
|
|
80
|
+
content.slice(index + normalizedOld.length),
|
|
81
|
+
occurrences,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tool, type Tool } from 'ai';
|
|
3
|
+
import { z } from 'zod/v3';
|
|
4
|
+
import DESCRIPTION from './edit.txt' with { type: 'text' };
|
|
5
|
+
import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
6
|
+
import { applyStringEdit } from './edit-shared.ts';
|
|
7
|
+
import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
|
|
8
|
+
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
9
|
+
|
|
10
|
+
export function buildEditTool(projectRoot: string): {
|
|
11
|
+
name: string;
|
|
12
|
+
tool: Tool;
|
|
13
|
+
} {
|
|
14
|
+
const edit = tool({
|
|
15
|
+
description: DESCRIPTION,
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
path: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe(
|
|
20
|
+
'Relative file path within the project. Absolute paths are not allowed.',
|
|
21
|
+
),
|
|
22
|
+
oldString: z.string().describe('Exact text to replace'),
|
|
23
|
+
newString: z.string().describe('Replacement text'),
|
|
24
|
+
replaceAll: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(false)
|
|
28
|
+
.describe(
|
|
29
|
+
'Replace every matching occurrence instead of requiring a unique match',
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
async execute({
|
|
33
|
+
path,
|
|
34
|
+
oldString,
|
|
35
|
+
newString,
|
|
36
|
+
replaceAll = false,
|
|
37
|
+
}: {
|
|
38
|
+
path: string;
|
|
39
|
+
oldString: string;
|
|
40
|
+
newString: string;
|
|
41
|
+
replaceAll?: boolean;
|
|
42
|
+
}): Promise<
|
|
43
|
+
ToolResponse<{
|
|
44
|
+
path: string;
|
|
45
|
+
occurrences: number;
|
|
46
|
+
bytes: number;
|
|
47
|
+
artifact: unknown;
|
|
48
|
+
}>
|
|
49
|
+
> {
|
|
50
|
+
if (!path || path.trim().length === 0) {
|
|
51
|
+
return createToolError(
|
|
52
|
+
'Missing required parameter: path',
|
|
53
|
+
'validation',
|
|
54
|
+
{
|
|
55
|
+
parameter: 'path',
|
|
56
|
+
value: path,
|
|
57
|
+
suggestion: 'Provide a relative file path to edit',
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (isAbsoluteLike(path)) {
|
|
62
|
+
return createToolError(
|
|
63
|
+
`Refusing to edit outside project root: ${path}`,
|
|
64
|
+
'permission',
|
|
65
|
+
{
|
|
66
|
+
parameter: 'path',
|
|
67
|
+
value: path,
|
|
68
|
+
suggestion: 'Use a relative path within the project',
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const abs = resolveSafePath(projectRoot, path);
|
|
74
|
+
try {
|
|
75
|
+
await assertFreshRead(projectRoot, abs, path);
|
|
76
|
+
const original = await readFile(abs, 'utf-8');
|
|
77
|
+
const updated = applyStringEdit(
|
|
78
|
+
original,
|
|
79
|
+
oldString,
|
|
80
|
+
newString,
|
|
81
|
+
replaceAll,
|
|
82
|
+
);
|
|
83
|
+
if (updated.content === original) {
|
|
84
|
+
return createToolError('No changes applied.', 'validation', {
|
|
85
|
+
suggestion:
|
|
86
|
+
'Adjust oldString/newString so the file content actually changes',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await writeFile(abs, updated.content, 'utf-8');
|
|
91
|
+
await rememberFileWrite(projectRoot, abs);
|
|
92
|
+
const artifact = await buildWriteArtifact(
|
|
93
|
+
path,
|
|
94
|
+
true,
|
|
95
|
+
original,
|
|
96
|
+
updated.content,
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
path,
|
|
101
|
+
occurrences: updated.occurrences,
|
|
102
|
+
bytes: updated.content.length,
|
|
103
|
+
artifact,
|
|
104
|
+
};
|
|
105
|
+
} catch (error: unknown) {
|
|
106
|
+
const isEnoent =
|
|
107
|
+
error &&
|
|
108
|
+
typeof error === 'object' &&
|
|
109
|
+
'code' in error &&
|
|
110
|
+
error.code === 'ENOENT';
|
|
111
|
+
return createToolError(
|
|
112
|
+
isEnoent
|
|
113
|
+
? `File not found: ${path}`
|
|
114
|
+
: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
isEnoent ? 'not_found' : 'execution',
|
|
116
|
+
{
|
|
117
|
+
parameter: 'path',
|
|
118
|
+
value: path,
|
|
119
|
+
suggestion: isEnoent
|
|
120
|
+
? 'Use read or ls to confirm the file path first'
|
|
121
|
+
: undefined,
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return { name: 'edit', tool: edit };
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Replace an exact text block in an existing file.
|
|
2
|
+
|
|
3
|
+
Use this for targeted edits instead of apply_patch whenever possible.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- You must read the file first in the current session before editing it.
|
|
7
|
+
- `oldString` must match the file exactly, including whitespace.
|
|
8
|
+
- If `oldString` appears multiple times, provide more context or set `replaceAll: true`.
|
|
9
|
+
- Use `write` for new files and `apply_patch` for structural multi-file diffs.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Tool } from 'ai';
|
|
2
|
+
import { buildEditTool } from './edit.ts';
|
|
2
3
|
import { buildReadTool } from './read.ts';
|
|
4
|
+
import { buildMultiEditTool } from './multiedit.ts';
|
|
3
5
|
import { buildWriteTool } from './write.ts';
|
|
4
6
|
import { buildLsTool } from './ls.ts';
|
|
5
7
|
import { buildTreeTool } from './tree.ts';
|
|
@@ -11,6 +13,8 @@ export function buildFsTools(
|
|
|
11
13
|
): Array<{ name: string; tool: Tool }> {
|
|
12
14
|
const out: Array<{ name: string; tool: Tool }> = [];
|
|
13
15
|
out.push(buildReadTool(projectRoot));
|
|
16
|
+
out.push(buildEditTool(projectRoot));
|
|
17
|
+
out.push(buildMultiEditTool(projectRoot));
|
|
14
18
|
out.push(buildWriteTool(projectRoot));
|
|
15
19
|
out.push(buildLsTool(projectRoot));
|
|
16
20
|
out.push(buildTreeTool(projectRoot));
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tool, type Tool } from 'ai';
|
|
3
|
+
import { z } from 'zod/v3';
|
|
4
|
+
import DESCRIPTION from './multiedit.txt' with { type: 'text' };
|
|
5
|
+
import { buildWriteArtifact, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
6
|
+
import { applyStringEdit } from './edit-shared.ts';
|
|
7
|
+
import { assertFreshRead, rememberFileWrite } from './read-tracker.ts';
|
|
8
|
+
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
9
|
+
|
|
10
|
+
const multiEditSchema = z.object({
|
|
11
|
+
path: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe(
|
|
14
|
+
'Relative file path within the project. Absolute paths are not allowed.',
|
|
15
|
+
),
|
|
16
|
+
edits: z
|
|
17
|
+
.array(
|
|
18
|
+
z.object({
|
|
19
|
+
oldString: z.string().describe('Exact text to replace'),
|
|
20
|
+
newString: z.string().describe('Replacement text'),
|
|
21
|
+
replaceAll: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.default(false)
|
|
25
|
+
.describe('Replace every matching occurrence for this edit'),
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
.min(1)
|
|
29
|
+
.describe('Edits to apply sequentially to the same file'),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export function buildMultiEditTool(projectRoot: string): {
|
|
33
|
+
name: string;
|
|
34
|
+
tool: Tool;
|
|
35
|
+
} {
|
|
36
|
+
const multiedit = tool({
|
|
37
|
+
description: DESCRIPTION,
|
|
38
|
+
inputSchema: multiEditSchema,
|
|
39
|
+
async execute({ path, edits }: z.infer<typeof multiEditSchema>): Promise<
|
|
40
|
+
ToolResponse<{
|
|
41
|
+
path: string;
|
|
42
|
+
editsApplied: number;
|
|
43
|
+
bytes: number;
|
|
44
|
+
artifact: unknown;
|
|
45
|
+
}>
|
|
46
|
+
> {
|
|
47
|
+
if (!path || path.trim().length === 0) {
|
|
48
|
+
return createToolError(
|
|
49
|
+
'Missing required parameter: path',
|
|
50
|
+
'validation',
|
|
51
|
+
{
|
|
52
|
+
parameter: 'path',
|
|
53
|
+
value: path,
|
|
54
|
+
suggestion: 'Provide a relative file path to edit',
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (isAbsoluteLike(path)) {
|
|
59
|
+
return createToolError(
|
|
60
|
+
`Refusing to edit outside project root: ${path}`,
|
|
61
|
+
'permission',
|
|
62
|
+
{
|
|
63
|
+
parameter: 'path',
|
|
64
|
+
value: path,
|
|
65
|
+
suggestion: 'Use a relative path within the project',
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const abs = resolveSafePath(projectRoot, path);
|
|
71
|
+
try {
|
|
72
|
+
await assertFreshRead(projectRoot, abs, path);
|
|
73
|
+
const original = await readFile(abs, 'utf-8');
|
|
74
|
+
let nextContent = original;
|
|
75
|
+
for (let i = 0; i < edits.length; i++) {
|
|
76
|
+
const edit = edits[i];
|
|
77
|
+
try {
|
|
78
|
+
nextContent = applyStringEdit(
|
|
79
|
+
nextContent,
|
|
80
|
+
edit.oldString,
|
|
81
|
+
edit.newString,
|
|
82
|
+
edit.replaceAll,
|
|
83
|
+
).content;
|
|
84
|
+
} catch (error: unknown) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Edit ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (nextContent === original) {
|
|
92
|
+
return createToolError('No changes applied.', 'validation', {
|
|
93
|
+
suggestion:
|
|
94
|
+
'Adjust your edits so the file content actually changes',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await writeFile(abs, nextContent, 'utf-8');
|
|
99
|
+
await rememberFileWrite(projectRoot, abs);
|
|
100
|
+
const artifact = await buildWriteArtifact(
|
|
101
|
+
path,
|
|
102
|
+
true,
|
|
103
|
+
original,
|
|
104
|
+
nextContent,
|
|
105
|
+
);
|
|
106
|
+
return {
|
|
107
|
+
ok: true,
|
|
108
|
+
path,
|
|
109
|
+
editsApplied: edits.length,
|
|
110
|
+
bytes: nextContent.length,
|
|
111
|
+
artifact,
|
|
112
|
+
};
|
|
113
|
+
} catch (error: unknown) {
|
|
114
|
+
const isEnoent =
|
|
115
|
+
error &&
|
|
116
|
+
typeof error === 'object' &&
|
|
117
|
+
'code' in error &&
|
|
118
|
+
error.code === 'ENOENT';
|
|
119
|
+
return createToolError(
|
|
120
|
+
isEnoent
|
|
121
|
+
? `File not found: ${path}`
|
|
122
|
+
: `Failed to edit file: ${error instanceof Error ? error.message : String(error)}`,
|
|
123
|
+
isEnoent ? 'not_found' : 'execution',
|
|
124
|
+
{
|
|
125
|
+
parameter: 'path',
|
|
126
|
+
value: path,
|
|
127
|
+
suggestion: isEnoent
|
|
128
|
+
? 'Use read or ls to confirm the file path first'
|
|
129
|
+
: undefined,
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return { name: 'multiedit', tool: multiedit };
|
|
137
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Apply multiple exact text replacements to a single existing file atomically.
|
|
2
|
+
|
|
3
|
+
Use this when you need several edits in one file.
|
|
4
|
+
|
|
5
|
+
Rules:
|
|
6
|
+
- Read the file first before editing.
|
|
7
|
+
- Each edit uses exact `oldString` -> `newString` replacement.
|
|
8
|
+
- All edits must succeed or none are written.
|
|
9
|
+
- Use `replaceAll: true` only when every occurrence should change.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { resolve as resolvePath } from 'node:path';
|
|
3
|
+
|
|
4
|
+
type FileStamp = {
|
|
5
|
+
readAt: number;
|
|
6
|
+
mtimeMs?: number;
|
|
7
|
+
ctimeMs?: number;
|
|
8
|
+
size?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const readState = new Map<string, Map<string, FileStamp>>();
|
|
12
|
+
|
|
13
|
+
function getProjectState(projectRoot: string): Map<string, FileStamp> {
|
|
14
|
+
const key = resolvePath(projectRoot);
|
|
15
|
+
let state = readState.get(key);
|
|
16
|
+
if (!state) {
|
|
17
|
+
state = new Map<string, FileStamp>();
|
|
18
|
+
readState.set(key, state);
|
|
19
|
+
}
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function captureFileStamp(absPath: string): Promise<FileStamp> {
|
|
24
|
+
const stats = await stat(absPath);
|
|
25
|
+
return {
|
|
26
|
+
readAt: Date.now(),
|
|
27
|
+
mtimeMs: Number.isFinite(stats.mtimeMs) ? stats.mtimeMs : undefined,
|
|
28
|
+
ctimeMs: Number.isFinite(stats.ctimeMs) ? stats.ctimeMs : undefined,
|
|
29
|
+
size: typeof stats.size === 'number' ? stats.size : undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function rememberFileRead(
|
|
34
|
+
projectRoot: string,
|
|
35
|
+
absPath: string,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const state = getProjectState(projectRoot);
|
|
38
|
+
state.set(absPath, await captureFileStamp(absPath));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function rememberFileWrite(
|
|
42
|
+
projectRoot: string,
|
|
43
|
+
absPath: string,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const state = getProjectState(projectRoot);
|
|
46
|
+
state.set(absPath, await captureFileStamp(absPath));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function assertFreshRead(
|
|
50
|
+
projectRoot: string,
|
|
51
|
+
absPath: string,
|
|
52
|
+
displayPath: string,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const state = getProjectState(projectRoot);
|
|
55
|
+
const previous = state.get(absPath);
|
|
56
|
+
if (!previous) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`You must read file ${displayPath} before editing it. Use the read tool first.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const current = await captureFileStamp(absPath);
|
|
63
|
+
const changed =
|
|
64
|
+
current.mtimeMs !== previous.mtimeMs ||
|
|
65
|
+
current.ctimeMs !== previous.ctimeMs ||
|
|
66
|
+
current.size !== previous.size;
|
|
67
|
+
if (!changed) return;
|
|
68
|
+
|
|
69
|
+
throw new Error(
|
|
70
|
+
`File ${displayPath} has changed since it was last read. Read it again before editing.`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -2,6 +2,7 @@ import { tool, type Tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod/v3';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
5
|
+
import { rememberFileRead } from './read-tracker.ts';
|
|
5
6
|
import DESCRIPTION from './read.txt' with { type: 'text' };
|
|
6
7
|
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
7
8
|
|
|
@@ -108,6 +109,7 @@ export function buildReadTool(projectRoot: string): {
|
|
|
108
109
|
try {
|
|
109
110
|
let content = await readFile(abs, 'utf-8');
|
|
110
111
|
const indent = detectIndentation(content);
|
|
112
|
+
await rememberFileRead(projectRoot, abs);
|
|
111
113
|
|
|
112
114
|
if (startLine !== undefined && endLine !== undefined) {
|
|
113
115
|
const lines = content.split('\n');
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
expandTilde,
|
|
9
9
|
isAbsoluteLike,
|
|
10
10
|
} from './util.ts';
|
|
11
|
+
import { rememberFileWrite } from './read-tracker.ts';
|
|
11
12
|
import DESCRIPTION from './write.txt' with { type: 'text' };
|
|
12
13
|
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
13
14
|
|
|
@@ -79,6 +80,7 @@ export function buildWriteTool(projectRoot: string): {
|
|
|
79
80
|
existed = true;
|
|
80
81
|
} catch {}
|
|
81
82
|
await writeFile(abs, content);
|
|
83
|
+
await rememberFileWrite(projectRoot, abs);
|
|
82
84
|
const artifact = await buildWriteArtifact(
|
|
83
85
|
req,
|
|
84
86
|
existed,
|
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
Usage tips:
|
|
7
7
|
- Use for creating NEW files
|
|
8
8
|
- Use when replacing >70% of a file's content (almost complete rewrite)
|
|
9
|
-
- NEVER use for partial/targeted edits - use apply_patch
|
|
9
|
+
- NEVER use for partial/targeted edits - use edit/multiedit first, or apply_patch for structural changes
|
|
10
10
|
- Using write for partial edits wastes output tokens and risks hallucinating unchanged parts
|
|
11
11
|
- Prefer idempotent writes by providing the full intended content when you do use write
|
|
@@ -12,6 +12,13 @@ You help with coding and build tasks.
|
|
|
12
12
|
- Use `terminal(operation: "write", input: "\u0003")` or `terminal(operation: "interrupt")` to stop a process before resorting to `kill`.
|
|
13
13
|
- Summarize active terminals (purpose, key command, port) in your updates so collaborators know what's running.
|
|
14
14
|
|
|
15
|
+
## Preferred Editing Order
|
|
16
|
+
|
|
17
|
+
- Use `edit` for one exact replacement in an existing file.
|
|
18
|
+
- Use `multiedit` for several exact replacements in the same file.
|
|
19
|
+
- Use `apply_patch` for structural or multi-file changes, file add/delete/rename, or edits that are awkward as exact replacements.
|
|
20
|
+
- Use `write` for new files or near-total rewrites.
|
|
21
|
+
|
|
15
22
|
## apply_patch — Mandatory Rules
|
|
16
23
|
|
|
17
24
|
These rules apply EVERY time you use the `apply_patch` tool. Violations cause patch failures.
|
|
@@ -211,7 +218,7 @@ Key points:
|
|
|
211
218
|
- Wastes output tokens and risks hallucinating unchanged parts
|
|
212
219
|
|
|
213
220
|
## Never
|
|
214
|
-
- Use `write` for partial file edits (use `apply_patch`
|
|
221
|
+
- Use `write` for partial file edits (use `edit`/`multiedit` first, or `apply_patch` for structural diffs)
|
|
215
222
|
- Make multiple separate `apply_patch` calls for the same file (use multiple hunks with @@ headers instead)
|
|
216
223
|
- Assume file content remains unchanged between operations
|
|
217
|
-
- Use `bash` with `sed`/`awk` for programmatic file editing (use `apply_patch` instead)
|
|
224
|
+
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit`, `multiedit`, or `apply_patch` instead)
|
package/src/prompts/src/base.txt
CHANGED
|
@@ -21,10 +21,13 @@ You MUST call the `finish` tool at the end of every response to signal completio
|
|
|
21
21
|
**IMPORTANT**: Do NOT call `finish` before streaming your response. Always stream your message first, then call `finish`. If you forget to call `finish`, the system will hang and not complete properly.
|
|
22
22
|
|
|
23
23
|
File Editing Best Practices:
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
24
|
+
- Prefer `edit` for one exact replacement in an existing file
|
|
25
|
+
- Prefer `multiedit` for several exact replacements in the same file
|
|
26
|
+
- Use `apply_patch` for structural diffs, file add/delete/rename, or changes that are awkward as exact replacements
|
|
27
|
+
- ALWAYS read a file immediately before using `edit`, `multiedit`, or `apply_patch`
|
|
28
|
+
- For `edit` / `multiedit`, copy the exact text including whitespace from the latest `read` output
|
|
29
|
+
- For `apply_patch`, use the `indentation` field from `read` and copy context lines CHARACTER-FOR-CHARACTER
|
|
30
|
+
- When making multiple edits to the same file with `apply_patch`, use multiple `@@` hunks in a single call
|
|
31
|
+
- Never assume file content remains unchanged between separate edit operations
|
|
32
|
+
- If an edit tool fails, read the file AGAIN and copy the exact lines
|
|
33
|
+
- If `apply_patch` fails repeatedly on the same file, prefer `edit` / `multiedit` when possible and fall back to `write` only for a full-file rewrite
|
|
@@ -20,7 +20,9 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
20
20
|
**File Reading & Editing**:
|
|
21
21
|
- `read`: Read file contents (supports line ranges)
|
|
22
22
|
- `write`: Write complete file contents
|
|
23
|
-
- `
|
|
23
|
+
- `edit`: Exact text replacement in one file
|
|
24
|
+
- `multiedit`: Multiple exact replacements in one file
|
|
25
|
+
- `apply_patch`: Apply diff/enveloped patches (best for structural or multi-file changes)
|
|
24
26
|
|
|
25
27
|
**Version Control**:
|
|
26
28
|
- `git_status`, `git_diff`
|
|
@@ -34,12 +36,13 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
34
36
|
### Tool Usage Best Practices:
|
|
35
37
|
|
|
36
38
|
1. **Batch Independent Operations**: Make all independent tool calls in one turn
|
|
37
|
-
2. **File Editing**: Prefer `
|
|
38
|
-
3. **
|
|
39
|
-
4. **
|
|
40
|
-
5. **
|
|
41
|
-
6. **
|
|
42
|
-
7. **
|
|
39
|
+
2. **File Editing**: Prefer `edit` for one targeted replacement and `multiedit` for several exact replacements in the same file
|
|
40
|
+
3. **Structural Changes**: Use `apply_patch` for multi-file diffs, file add/delete/rename, or edits that are awkward as exact replacements
|
|
41
|
+
4. **Patch Consolidation**: When you do use `apply_patch` multiple times on the same file, use multiple `@@` hunks in ONE `apply_patch` call
|
|
42
|
+
5. **Search First**: Use `glob` to find files before reading them
|
|
43
|
+
6. **Progress Updates**: Call `progress_update` at major milestones (planning, discovering, writing, verifying)
|
|
44
|
+
7. **Plan Tracking**: Use `update_todos` to show task breakdown and progress
|
|
45
|
+
8. **Finish Required**: Always call `finish` tool when complete
|
|
43
46
|
|
|
44
47
|
### Tool Failure Handling
|
|
45
48
|
|
|
@@ -49,10 +52,18 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
49
52
|
|
|
50
53
|
## File Editing Best Practices
|
|
51
54
|
|
|
52
|
-
**Using the `
|
|
55
|
+
**Using the `edit` / `multiedit` Tools** (Recommended):
|
|
56
|
+
- Use `edit` for one exact replacement in an existing file
|
|
57
|
+
- Use `multiedit` for several exact replacements in the same file
|
|
58
|
+
- These tools are the default choice for targeted edits because they avoid patch-formatting mistakes
|
|
59
|
+
- Read the file first, then copy the exact text including whitespace
|
|
60
|
+
- If the text appears multiple times, include more surrounding context or use `replaceAll: true`
|
|
61
|
+
- Use `apply_patch` only when the change is structural, spans multiple files, or cannot be expressed as an exact replacement
|
|
62
|
+
|
|
63
|
+
**Using the `apply_patch` Tool** (Structural / Advanced):
|
|
53
64
|
- **CRITICAL**: ALWAYS read the target file immediately before creating a patch - never patch from memory
|
|
54
65
|
- The `read` tool returns an `indentation` field (e.g., "tabs", "2 spaces") — use it to match the file's indent style in your patch
|
|
55
|
-
-
|
|
66
|
+
- Use when the change is structural, multi-file, or awkward as an exact replacement
|
|
56
67
|
- Preferred format is the enveloped patch (`*** Begin Patch` ...); standard unified diffs (`---/+++`) are also accepted and auto-converted if provided
|
|
57
68
|
- Only requires the specific lines you want to change
|
|
58
69
|
- Format: `*** Begin Patch` ... `*** Update File: path` ... `-old` / `+new` ... `*** End Patch`
|
|
@@ -63,8 +74,8 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
63
74
|
- If patch fails, it means the file content doesn't match - read it again and retry
|
|
64
75
|
- If you suspect parts of the patch might be stale, set `allowRejects: true` so the tool applies what it can and reports the skipped hunks with reasons
|
|
65
76
|
- The tool quietly skips removal lines that are already gone and additions that already exist, so you don't need to resend the same change
|
|
66
|
-
- **Best for**:
|
|
67
|
-
- **Struggles with**:
|
|
77
|
+
- **Best for**: Structural diffs, file add/delete/rename, or edits that don't fit exact string replacement cleanly
|
|
78
|
+
- **Struggles with**: Small targeted edits where `edit` or `multiedit` would be simpler and less error-prone
|
|
68
79
|
|
|
69
80
|
**Patch Format Reminder**:
|
|
70
81
|
```
|
|
@@ -83,10 +94,10 @@ You have access to a rich set of specialized tools optimized for coding tasks:
|
|
|
83
94
|
- Wastes output tokens and risks hallucinating unchanged parts
|
|
84
95
|
|
|
85
96
|
**Never**:
|
|
86
|
-
- Use `write` for partial file edits (use `apply_patch`
|
|
97
|
+
- Use `write` for partial file edits (use `edit`/`multiedit` first, or `apply_patch` for structural diffs)
|
|
87
98
|
- Make multiple separate `apply_patch` calls for the same file (use multiple hunks with @@ headers instead)
|
|
88
99
|
- Assume file content remains unchanged between operations
|
|
89
|
-
- Use `bash` with `sed`/`awk` for programmatic file editing (use `apply_patch` instead)
|
|
100
|
+
- Use `bash` with `sed`/`awk` for programmatic file editing (use `edit`, `multiedit`, or `apply_patch` instead)
|
|
90
101
|
|
|
91
102
|
## Direct File References
|
|
92
103
|
|
|
@@ -264,7 +275,7 @@ You MUST adhere to the following criteria when solving queries:
|
|
|
264
275
|
- Working on the repo(s) in the current environment is allowed, even if they are proprietary.
|
|
265
276
|
- Analyzing code for vulnerabilities is allowed.
|
|
266
277
|
- Showing user code and tool call details is allowed.
|
|
267
|
-
-
|
|
278
|
+
- Prefer `edit` and `multiedit` for targeted changes in existing files. Use `apply_patch` for structural diffs or file add/delete/rename. When using patches, NEVER try `applypatch` or `apply-patch`, only `apply_patch`.
|
|
268
279
|
|
|
269
280
|
If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines:
|
|
270
281
|
|