@oh-my-pi/pi-coding-agent 3.6.1337 → 3.9.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 +39 -0
- package/package.json +4 -4
- package/src/core/bash-executor.ts +115 -154
- package/src/core/index.ts +2 -0
- package/src/core/session-manager.ts +16 -6
- package/src/core/settings-manager.ts +2 -2
- 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 +66 -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/{footer.ts → status-line.ts} +124 -71
- package/src/modes/interactive/components/tool-execution.ts +14 -14
- package/src/modes/interactive/interactive-mode.ts +57 -73
- package/src/modes/interactive/theme/dark.json +13 -13
- package/src/modes/interactive/theme/light.json +13 -13
- package/src/modes/interactive/theme/theme.ts +29 -28
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
|
}
|
package/src/core/tools/find.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { globSync } from "glob";
|
|
6
6
|
import { ensureTool } from "../../utils/tools-manager";
|
|
7
|
+
import { untilAborted } from "../utils";
|
|
7
8
|
import { resolveToCwd } from "./path-utils";
|
|
8
9
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate";
|
|
9
10
|
|
|
@@ -67,191 +68,171 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
|
|
|
67
68
|
},
|
|
68
69
|
signal?: AbortSignal,
|
|
69
70
|
) => {
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
return untilAborted(signal, async () => {
|
|
72
|
+
// Ensure fd is available
|
|
73
|
+
const fdPath = await ensureTool("fd", true);
|
|
74
|
+
if (!fdPath) {
|
|
75
|
+
throw new Error("fd is not available and could not be downloaded");
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
+
const searchPath = resolveToCwd(searchDir || ".", cwd);
|
|
79
|
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
80
|
+
const effectiveType = type ?? "all";
|
|
81
|
+
const includeHidden = hidden ?? false;
|
|
82
|
+
const shouldSortByMtime = sortByMtime ?? false;
|
|
83
|
+
|
|
84
|
+
// Build fd arguments
|
|
85
|
+
const args: string[] = [
|
|
86
|
+
"--glob", // Use glob pattern
|
|
87
|
+
"--color=never", // No ANSI colors
|
|
88
|
+
"--max-results",
|
|
89
|
+
String(effectiveLimit),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (includeHidden) {
|
|
93
|
+
args.push("--hidden");
|
|
94
|
+
}
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
96
|
+
// Add type filter
|
|
97
|
+
if (effectiveType === "file") {
|
|
98
|
+
args.push("--type", "f");
|
|
99
|
+
} else if (effectiveType === "dir") {
|
|
100
|
+
args.push("--type", "d");
|
|
101
|
+
}
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Build fd arguments
|
|
95
|
-
const args: string[] = [
|
|
96
|
-
"--glob", // Use glob pattern
|
|
97
|
-
"--color=never", // No ANSI colors
|
|
98
|
-
"--max-results",
|
|
99
|
-
String(effectiveLimit),
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
if (includeHidden) {
|
|
103
|
-
args.push("--hidden");
|
|
104
|
-
}
|
|
103
|
+
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
104
|
+
const gitignoreFiles = new Set<string>();
|
|
105
|
+
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
106
|
+
if (existsSync(rootGitignore)) {
|
|
107
|
+
gitignoreFiles.add(rootGitignore);
|
|
108
|
+
}
|
|
105
109
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
try {
|
|
111
|
+
const nestedGitignores = globSync("**/.gitignore", {
|
|
112
|
+
cwd: searchPath,
|
|
113
|
+
dot: true,
|
|
114
|
+
absolute: true,
|
|
115
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
116
|
+
});
|
|
117
|
+
for (const file of nestedGitignores) {
|
|
118
|
+
gitignoreFiles.add(file);
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore glob errors
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (existsSync(rootGitignore)) {
|
|
117
|
-
gitignoreFiles.add(rootGitignore);
|
|
118
|
-
}
|
|
124
|
+
for (const gitignorePath of gitignoreFiles) {
|
|
125
|
+
args.push("--ignore-file", gitignorePath);
|
|
126
|
+
}
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
cwd: searchPath,
|
|
123
|
-
dot: true,
|
|
124
|
-
absolute: true,
|
|
125
|
-
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
126
|
-
});
|
|
127
|
-
for (const file of nestedGitignores) {
|
|
128
|
-
gitignoreFiles.add(file);
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// Ignore glob errors
|
|
132
|
-
}
|
|
128
|
+
// Pattern and path
|
|
129
|
+
args.push(pattern, searchPath);
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
// Run fd
|
|
132
|
+
const result = Bun.spawnSync([fdPath, ...args], {
|
|
133
|
+
stdin: "ignore",
|
|
134
|
+
stdout: "pipe",
|
|
135
|
+
stderr: "pipe",
|
|
136
|
+
});
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
args.push(pattern, searchPath);
|
|
138
|
+
const output = result.stdout.toString().trim();
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
if (result.exitCode !== 0) {
|
|
141
|
+
const errorMsg = result.stderr.toString().trim() || `fd exited with code ${result.exitCode}`;
|
|
142
|
+
// fd returns non-zero for some errors but may still have partial output
|
|
143
|
+
if (!output) {
|
|
144
|
+
throw new Error(errorMsg);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
if (!output) {
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
|
151
|
+
details: { fileCount: 0, files: [], truncated: false },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
const lines = output.split("\n");
|
|
156
|
+
const relativized: string[] = [];
|
|
157
|
+
const mtimes: number[] = [];
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
159
|
+
for (const rawLine of lines) {
|
|
160
|
+
const line = rawLine.replace(/\r$/, "").trim();
|
|
161
|
+
if (!line) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
166
|
+
let relativePath = line;
|
|
167
|
+
if (line.startsWith(searchPath)) {
|
|
168
|
+
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
169
|
+
} else {
|
|
170
|
+
relativePath = path.relative(searchPath, line);
|
|
171
|
+
}
|
|
168
172
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const line = rawLine.replace(/\r$/, "").trim();
|
|
175
|
-
if (!line) {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
|
|
180
|
-
let relativePath = line;
|
|
181
|
-
if (line.startsWith(searchPath)) {
|
|
182
|
-
relativePath = line.slice(searchPath.length + 1); // +1 for the /
|
|
183
|
-
} else {
|
|
184
|
-
relativePath = path.relative(searchPath, line);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
188
|
-
relativePath += "/";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
relativized.push(relativePath);
|
|
192
|
-
|
|
193
|
-
// Collect mtime if sorting is requested
|
|
194
|
-
if (shouldSortByMtime) {
|
|
195
|
-
try {
|
|
196
|
-
const fullPath = path.join(searchPath, relativePath);
|
|
197
|
-
const stat: Stats = statSync(fullPath);
|
|
198
|
-
mtimes.push(stat.mtimeMs);
|
|
199
|
-
} catch {
|
|
200
|
-
mtimes.push(0);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
173
|
+
if (hadTrailingSlash && !relativePath.endsWith("/")) {
|
|
174
|
+
relativePath += "/";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
relativized.push(relativePath);
|
|
204
178
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
179
|
+
// Collect mtime if sorting is requested
|
|
180
|
+
if (shouldSortByMtime) {
|
|
181
|
+
try {
|
|
182
|
+
const fullPath = path.join(searchPath, relativePath);
|
|
183
|
+
const stat: Stats = statSync(fullPath);
|
|
184
|
+
mtimes.push(stat.mtimeMs);
|
|
185
|
+
} catch {
|
|
186
|
+
mtimes.push(0);
|
|
211
187
|
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
212
190
|
|
|
213
|
-
|
|
214
|
-
|
|
191
|
+
// Sort by mtime if requested (most recent first)
|
|
192
|
+
if (shouldSortByMtime && relativized.length > 0) {
|
|
193
|
+
const indexed = relativized.map((path, idx) => ({ path, mtime: mtimes[idx] || 0 }));
|
|
194
|
+
indexed.sort((a, b) => b.mtime - a.mtime);
|
|
195
|
+
relativized.length = 0;
|
|
196
|
+
relativized.push(...indexed.map((item) => item.path));
|
|
197
|
+
}
|
|
215
198
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
199
|
+
// Check if we hit the result limit
|
|
200
|
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
|
219
201
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
files: relativized.slice(0, 50),
|
|
224
|
-
truncated: resultLimitReached || truncation.truncated,
|
|
225
|
-
};
|
|
202
|
+
// Apply byte truncation (no line limit since we already have result limit)
|
|
203
|
+
const rawOutput = relativized.join("\n");
|
|
204
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
226
205
|
|
|
227
|
-
|
|
228
|
-
|
|
206
|
+
let resultOutput = truncation.content;
|
|
207
|
+
const details: FindToolDetails = {
|
|
208
|
+
fileCount: relativized.length,
|
|
209
|
+
files: relativized.slice(0, 50),
|
|
210
|
+
truncated: resultLimitReached || truncation.truncated,
|
|
211
|
+
};
|
|
229
212
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
233
|
-
);
|
|
234
|
-
details.resultLimitReached = effectiveLimit;
|
|
235
|
-
}
|
|
213
|
+
// Build notices
|
|
214
|
+
const notices: string[] = [];
|
|
236
215
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
if (resultLimitReached) {
|
|
217
|
+
notices.push(
|
|
218
|
+
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
219
|
+
);
|
|
220
|
+
details.resultLimitReached = effectiveLimit;
|
|
221
|
+
}
|
|
241
222
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
223
|
+
if (truncation.truncated) {
|
|
224
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
225
|
+
details.truncation = truncation;
|
|
226
|
+
}
|
|
245
227
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})();
|
|
228
|
+
if (notices.length > 0) {
|
|
229
|
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: resultOutput }],
|
|
234
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
235
|
+
};
|
|
255
236
|
});
|
|
256
237
|
},
|
|
257
238
|
};
|