@oh-my-pi/pi-coding-agent 3.5.1337 → 3.8.1337
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/CHANGELOG.md +29 -0
- package/package.json +5 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +113 -74
- package/src/core/tools/edit-diff.ts +45 -33
- package/src/core/tools/edit.ts +70 -182
- package/src/core/tools/find.ts +141 -160
- package/src/core/tools/index.ts +10 -9
- package/src/core/tools/ls.ts +64 -82
- package/src/core/tools/lsp/client.ts +63 -0
- package/src/core/tools/lsp/edits.ts +13 -4
- package/src/core/tools/lsp/index.ts +191 -85
- package/src/core/tools/notebook.ts +89 -144
- package/src/core/tools/read.ts +110 -158
- package/src/core/tools/write.ts +22 -115
- package/src/core/utils.ts +187 -0
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +23 -54
- package/src/modes/rpc/rpc-mode.ts +8 -7
|
@@ -249,40 +249,52 @@ function findFirstDifferentLine(oldLines: string[], newLines: string[]): { oldLi
|
|
|
249
249
|
return { oldLine: oldLines[0] ?? "", newLine: newLines[0] ?? "" };
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
export
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
252
|
+
export class EditMatchError extends Error {
|
|
253
|
+
constructor(
|
|
254
|
+
public readonly path: string,
|
|
255
|
+
public readonly normalizedOldText: string,
|
|
256
|
+
public readonly closest: EditMatch | undefined,
|
|
257
|
+
public readonly options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
258
|
+
) {
|
|
259
|
+
super(EditMatchError.formatMessage(path, normalizedOldText, closest, options));
|
|
260
|
+
this.name = "EditMatchError";
|
|
262
261
|
}
|
|
263
262
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
263
|
+
static formatMessage(
|
|
264
|
+
path: string,
|
|
265
|
+
normalizedOldText: string,
|
|
266
|
+
closest: EditMatch | undefined,
|
|
267
|
+
options: { allowFuzzy: boolean; similarityThreshold: number; fuzzyMatches?: number },
|
|
268
|
+
): string {
|
|
269
|
+
if (!closest) {
|
|
270
|
+
return options.allowFuzzy
|
|
271
|
+
? `Could not find a close enough match in ${path}.`
|
|
272
|
+
: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const similarity = Math.round(closest.confidence * 100);
|
|
276
|
+
const oldLines = normalizedOldText.split("\n");
|
|
277
|
+
const actualLines = closest.actualText.split("\n");
|
|
278
|
+
const { oldLine, newLine } = findFirstDifferentLine(oldLines, actualLines);
|
|
279
|
+
const thresholdPercent = Math.round(options.similarityThreshold * 100);
|
|
280
|
+
|
|
281
|
+
const hint = options.allowFuzzy
|
|
282
|
+
? options.fuzzyMatches && options.fuzzyMatches > 1
|
|
283
|
+
? `Found ${options.fuzzyMatches} high-confidence matches. Provide more context to make it unique.`
|
|
284
|
+
: `Closest match was below the ${thresholdPercent}% similarity threshold.`
|
|
285
|
+
: "Fuzzy matching is disabled. Enable 'Edit fuzzy match' in settings to accept high-confidence matches.";
|
|
286
|
+
|
|
287
|
+
return [
|
|
288
|
+
options.allowFuzzy
|
|
289
|
+
? `Could not find a close enough match in ${path}.`
|
|
290
|
+
: `Could not find the exact text in ${path}.`,
|
|
291
|
+
``,
|
|
292
|
+
`Closest match (${similarity}% similar) at line ${closest.startLine}:`,
|
|
293
|
+
` - ${oldLine}`,
|
|
294
|
+
` + ${newLine}`,
|
|
295
|
+
hint,
|
|
296
|
+
].join("\n");
|
|
297
|
+
}
|
|
286
298
|
}
|
|
287
299
|
|
|
288
300
|
/**
|
|
@@ -444,7 +456,7 @@ export async function computeEditDiff(
|
|
|
444
456
|
|
|
445
457
|
if (!matchOutcome.match) {
|
|
446
458
|
return {
|
|
447
|
-
error:
|
|
459
|
+
error: EditMatchError.formatMessage(path, normalizedOldText, matchOutcome.closest, {
|
|
448
460
|
allowFuzzy: fuzzy,
|
|
449
461
|
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
450
462
|
fuzzyMatches: matchOutcome.fuzzyMatches,
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { access, readFile, writeFile } from "node:fs/promises";
|
|
3
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
2
|
import { Type } from "@sinclair/typebox";
|
|
5
3
|
import {
|
|
6
4
|
DEFAULT_FUZZY_THRESHOLD,
|
|
7
5
|
detectLineEnding,
|
|
6
|
+
EditMatchError,
|
|
8
7
|
findEditMatch,
|
|
9
|
-
formatEditMatchError,
|
|
10
8
|
generateDiffString,
|
|
11
9
|
normalizeToLF,
|
|
12
10
|
restoreLineEndings,
|
|
13
11
|
stripBom,
|
|
14
12
|
} from "./edit-diff";
|
|
15
|
-
import type
|
|
13
|
+
import { type FileDiagnosticsResult, type WritethroughCallback, writethroughNoop } from "./lsp/index";
|
|
16
14
|
import { resolveToCwd } from "./path-utils";
|
|
17
15
|
|
|
18
16
|
const editSchema = Type.Object({
|
|
@@ -28,8 +26,6 @@ export interface EditToolDetails {
|
|
|
28
26
|
diff: string;
|
|
29
27
|
/** Line number of the first change in the new file (for editor navigation) */
|
|
30
28
|
firstChangedLine?: number;
|
|
31
|
-
/** Whether LSP diagnostics were retrieved */
|
|
32
|
-
hasDiagnostics?: boolean;
|
|
33
29
|
/** Diagnostic result (if available) */
|
|
34
30
|
diagnostics?: FileDiagnosticsResult;
|
|
35
31
|
}
|
|
@@ -37,12 +33,13 @@ export interface EditToolDetails {
|
|
|
37
33
|
export interface EditToolOptions {
|
|
38
34
|
/** Whether to accept high-confidence fuzzy matches for whitespace/indentation (default: true) */
|
|
39
35
|
fuzzyMatch?: boolean;
|
|
40
|
-
/**
|
|
41
|
-
|
|
36
|
+
/** Writethrough callback to get LSP diagnostics after editing a file */
|
|
37
|
+
writethrough?: WritethroughCallback;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
|
|
45
41
|
const allowFuzzy = options.fuzzyMatch ?? true;
|
|
42
|
+
const writethrough = options.writethrough ?? writethroughNoop;
|
|
46
43
|
return {
|
|
47
44
|
name: "edit",
|
|
48
45
|
label: "Edit",
|
|
@@ -61,196 +58,87 @@ Usage:
|
|
|
61
58
|
{ path, oldText, newText }: { path: string; oldText: string; newText: string },
|
|
62
59
|
signal?: AbortSignal,
|
|
63
60
|
) => {
|
|
64
|
-
const absolutePath = resolveToCwd(path, cwd);
|
|
65
|
-
|
|
66
61
|
// Reject .ipynb files - use NotebookEdit tool instead
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
content: [
|
|
70
|
-
{
|
|
71
|
-
type: "text",
|
|
72
|
-
text: "Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.",
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
details: undefined,
|
|
76
|
-
};
|
|
62
|
+
if (path.endsWith(".ipynb")) {
|
|
63
|
+
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
77
64
|
}
|
|
78
65
|
|
|
79
|
-
|
|
80
|
-
content: Array<{ type: "text"; text: string }>;
|
|
81
|
-
details: EditToolDetails | undefined;
|
|
82
|
-
}>((resolve, reject) => {
|
|
83
|
-
// Check if already aborted
|
|
84
|
-
if (signal?.aborted) {
|
|
85
|
-
reject(new Error("Operation aborted"));
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let aborted = false;
|
|
90
|
-
|
|
91
|
-
// Set up abort handler
|
|
92
|
-
const onAbort = () => {
|
|
93
|
-
aborted = true;
|
|
94
|
-
reject(new Error("Operation aborted"));
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (signal) {
|
|
98
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Perform the edit operation
|
|
102
|
-
(async () => {
|
|
103
|
-
try {
|
|
104
|
-
// Check if file exists
|
|
105
|
-
try {
|
|
106
|
-
await access(absolutePath, constants.R_OK | constants.W_OK);
|
|
107
|
-
} catch {
|
|
108
|
-
if (signal) {
|
|
109
|
-
signal.removeEventListener("abort", onAbort);
|
|
110
|
-
}
|
|
111
|
-
reject(new Error(`File not found: ${path}`));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check if aborted before reading
|
|
116
|
-
if (aborted) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Read the file
|
|
121
|
-
const rawContent = await readFile(absolutePath, "utf-8");
|
|
122
|
-
|
|
123
|
-
// Check if aborted after reading
|
|
124
|
-
if (aborted) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
|
129
|
-
const { bom, text: content } = stripBom(rawContent);
|
|
130
|
-
|
|
131
|
-
const originalEnding = detectLineEnding(content);
|
|
132
|
-
const normalizedContent = normalizeToLF(content);
|
|
133
|
-
const normalizedOldText = normalizeToLF(oldText);
|
|
134
|
-
const normalizedNewText = normalizeToLF(newText);
|
|
135
|
-
|
|
136
|
-
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
137
|
-
allowFuzzy,
|
|
138
|
-
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
142
|
-
if (signal) {
|
|
143
|
-
signal.removeEventListener("abort", onAbort);
|
|
144
|
-
}
|
|
145
|
-
reject(
|
|
146
|
-
new Error(
|
|
147
|
-
`Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!matchOutcome.match) {
|
|
154
|
-
if (signal) {
|
|
155
|
-
signal.removeEventListener("abort", onAbort);
|
|
156
|
-
}
|
|
157
|
-
reject(
|
|
158
|
-
new Error(
|
|
159
|
-
formatEditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
160
|
-
allowFuzzy,
|
|
161
|
-
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
162
|
-
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
163
|
-
}),
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
66
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
168
67
|
|
|
169
|
-
|
|
68
|
+
const file = Bun.file(absolutePath);
|
|
69
|
+
if (!(await file.exists())) {
|
|
70
|
+
throw new Error(`File not found: ${path}`);
|
|
71
|
+
}
|
|
170
72
|
|
|
171
|
-
|
|
172
|
-
if (aborted) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
73
|
+
const rawContent = await file.text();
|
|
175
74
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
normalizedNewText +
|
|
179
|
-
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
75
|
+
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
|
76
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
180
77
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
reject(
|
|
187
|
-
new Error(
|
|
188
|
-
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
189
|
-
),
|
|
190
|
-
);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
78
|
+
const originalEnding = detectLineEnding(content);
|
|
79
|
+
const normalizedContent = normalizeToLF(content);
|
|
80
|
+
const normalizedOldText = normalizeToLF(oldText);
|
|
81
|
+
const normalizedNewText = normalizeToLF(newText);
|
|
193
82
|
|
|
194
|
-
|
|
195
|
-
|
|
83
|
+
const matchOutcome = findEditMatch(normalizedContent, normalizedOldText, {
|
|
84
|
+
allowFuzzy,
|
|
85
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
86
|
+
});
|
|
196
87
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
88
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Found ${matchOutcome.occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
201
93
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
94
|
+
if (!matchOutcome.match) {
|
|
95
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
96
|
+
allowFuzzy,
|
|
97
|
+
similarityThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
98
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
206
101
|
|
|
207
|
-
|
|
102
|
+
const match = matchOutcome.match;
|
|
103
|
+
const normalizedNewContent =
|
|
104
|
+
normalizedContent.substring(0, match.startIndex) +
|
|
105
|
+
normalizedNewText +
|
|
106
|
+
normalizedContent.substring(match.startIndex + match.actualText.length);
|
|
107
|
+
|
|
108
|
+
// Verify the replacement actually changed something
|
|
109
|
+
if (normalizedContent === normalizedNewContent) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
208
114
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (options.getDiagnostics) {
|
|
212
|
-
try {
|
|
213
|
-
diagnosticsResult = await options.getDiagnostics(absolutePath);
|
|
214
|
-
} catch {
|
|
215
|
-
// Ignore diagnostics errors - don't fail the edit
|
|
216
|
-
}
|
|
217
|
-
}
|
|
115
|
+
const finalContent = bom + restoreLineEndings(normalizedNewContent, originalEnding);
|
|
116
|
+
const diagnostics = await writethrough(absolutePath, finalContent, signal, file);
|
|
218
117
|
|
|
219
|
-
|
|
220
|
-
let resultText = `Successfully replaced text in ${path}.`;
|
|
118
|
+
const diffResult = generateDiffString(normalizedContent, normalizedNewContent);
|
|
221
119
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
resultText += `\n\nLSP Diagnostics (${diagnosticsResult.summary}):\n`;
|
|
225
|
-
resultText += diagnosticsResult.diagnostics.map((d) => ` ${d}`).join("\n");
|
|
226
|
-
}
|
|
120
|
+
// Build result text
|
|
121
|
+
let resultText = `Successfully replaced text in ${path}.`;
|
|
227
122
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
},
|
|
234
|
-
],
|
|
235
|
-
details: {
|
|
236
|
-
diff: diffResult.diff,
|
|
237
|
-
firstChangedLine: diffResult.firstChangedLine,
|
|
238
|
-
hasDiagnostics: diagnosticsResult?.available ?? false,
|
|
239
|
-
diagnostics: diagnosticsResult,
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
} catch (error: any) {
|
|
243
|
-
// Clean up abort handler
|
|
244
|
-
if (signal) {
|
|
245
|
-
signal.removeEventListener("abort", onAbort);
|
|
246
|
-
}
|
|
123
|
+
const messages = diagnostics?.messages;
|
|
124
|
+
if (messages && messages.length > 0) {
|
|
125
|
+
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
126
|
+
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
127
|
+
}
|
|
247
128
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: resultText,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
details: {
|
|
137
|
+
diff: diffResult.diff,
|
|
138
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
139
|
+
diagnostics: diagnostics,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
254
142
|
},
|
|
255
143
|
};
|
|
256
144
|
}
|