@ottocode/sdk 0.1.236 → 0.1.242
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/config/src/index.ts +1 -0
- package/src/config/src/manager.ts +2 -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/agents/init.txt +24 -0
- 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 +1518 -1276
- package/src/providers/src/oauth-models.ts +2 -0
- package/src/providers/src/utils.ts +1 -1
- package/src/skills/loader.ts +1 -1
- package/src/skills/parser.ts +209 -24
- package/src/types/src/config.ts +2 -1
package/package.json
CHANGED
package/src/config/src/index.ts
CHANGED
|
@@ -74,11 +74,12 @@ export async function writeDefaults(
|
|
|
74
74
|
agent: string;
|
|
75
75
|
provider: ProviderId;
|
|
76
76
|
model: string;
|
|
77
|
-
toolApproval: 'auto' | 'dangerous' | 'all';
|
|
77
|
+
toolApproval: 'auto' | 'dangerous' | 'all' | 'yolo';
|
|
78
78
|
guidedMode: boolean;
|
|
79
79
|
reasoningText: boolean;
|
|
80
80
|
reasoningLevel: 'minimal' | 'low' | 'medium' | 'high' | 'max' | 'xhigh';
|
|
81
81
|
theme: string;
|
|
82
|
+
autoCompactThresholdTokens: number | null;
|
|
82
83
|
}>,
|
|
83
84
|
projectRoot?: string,
|
|
84
85
|
) {
|
|
@@ -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)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
You are the init agent.
|
|
2
|
+
|
|
3
|
+
Your sole purpose is to generate or refresh repository agent documentation for future coding agents.
|
|
4
|
+
|
|
5
|
+
## Core behavior
|
|
6
|
+
|
|
7
|
+
- Inspect the real repository structure before writing anything.
|
|
8
|
+
- Trust code, config, manifests, routes, schemas, and app structure more than existing markdown.
|
|
9
|
+
- Prefer a root `AGENTS.md` that acts as the routing/index doc.
|
|
10
|
+
- Create `.agents/*.md` docs only when they represent meaningfully distinct areas.
|
|
11
|
+
- Prefer a few strong docs over many tiny docs.
|
|
12
|
+
- Keep docs practical: architecture, important paths, workflow rules, and when to consult related docs.
|
|
13
|
+
|
|
14
|
+
## For monorepos
|
|
15
|
+
|
|
16
|
+
- Detect workspace boundaries and package responsibilities.
|
|
17
|
+
- Root `AGENTS.md` should tell future agents which `.agents` doc to read for tasks involving web, TUI, mobile, server/API, database, SDK/shared packages, or cross-cutting changes.
|
|
18
|
+
- Avoid splitting docs unless the codebase clearly has distinct domains.
|
|
19
|
+
|
|
20
|
+
## Output expectations
|
|
21
|
+
|
|
22
|
+
- Update existing agent docs when appropriate instead of duplicating them.
|
|
23
|
+
- Mention concrete file paths and package names.
|
|
24
|
+
- Keep the final user-facing summary concise and specific about what was generated.
|
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
|