pi-microsandbox 0.1.0
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/LICENSE +21 -0
- package/README.md +120 -0
- package/SECURITY.md +58 -0
- package/docs/commands.md +38 -0
- package/docs/configuration.md +80 -0
- package/docs/development.md +119 -0
- package/docs/getting-started.md +66 -0
- package/docs/images.md +190 -0
- package/docs/safety.md +39 -0
- package/docs/storage.md +57 -0
- package/docs/troubleshooting.md +20 -0
- package/extensions/pi-msb/command.ts +532 -0
- package/extensions/pi-msb/config.ts +771 -0
- package/extensions/pi-msb/control.ts +803 -0
- package/extensions/pi-msb/footer.ts +191 -0
- package/extensions/pi-msb/git.ts +256 -0
- package/extensions/pi-msb/index.ts +156 -0
- package/extensions/pi-msb/labels.ts +321 -0
- package/extensions/pi-msb/locks.ts +292 -0
- package/extensions/pi-msb/operations-exec.ts +434 -0
- package/extensions/pi-msb/operations.ts +321 -0
- package/extensions/pi-msb/prune.ts +232 -0
- package/extensions/pi-msb/sandbox-manager.ts +702 -0
- package/extensions/pi-msb/skill-access.ts +164 -0
- package/extensions/pi-msb/storage.ts +332 -0
- package/extensions/pi-msb/tools.ts +417 -0
- package/extensions/pi-msb/transport.ts +518 -0
- package/extensions/pi-msb/types.ts +436 -0
- package/package.json +74 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BashOperations,
|
|
3
|
+
FindOperations,
|
|
4
|
+
GrepOperations,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type {
|
|
7
|
+
GrepFormattingHelpers,
|
|
8
|
+
SandboxGrepExecute,
|
|
9
|
+
SandboxTransport,
|
|
10
|
+
ToolOpsProvider,
|
|
11
|
+
} from "./types.ts";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_GREP_LIMIT = 100;
|
|
14
|
+
const BASH_COMMAND = "bash";
|
|
15
|
+
const RG_COMMAND = "rg";
|
|
16
|
+
|
|
17
|
+
function posix(value: string): string {
|
|
18
|
+
return value.replaceAll("\\", "/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeRegex(value: string): string {
|
|
22
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function braceAlternatives(pattern: string): string[] | null {
|
|
26
|
+
let open = -1;
|
|
27
|
+
let depth = 0;
|
|
28
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
29
|
+
const character = pattern[index];
|
|
30
|
+
if (character === "\\") {
|
|
31
|
+
index++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (character === "{" && depth++ === 0) open = index;
|
|
35
|
+
if (character === "}" && depth > 0 && --depth === 0) {
|
|
36
|
+
const inside = pattern.slice(open + 1, index);
|
|
37
|
+
const parts: string[] = [];
|
|
38
|
+
let partStart = 0;
|
|
39
|
+
let partDepth = 0;
|
|
40
|
+
for (let partIndex = 0; partIndex < inside.length; partIndex++) {
|
|
41
|
+
const partCharacter = inside[partIndex];
|
|
42
|
+
if (partCharacter === "{") partDepth++;
|
|
43
|
+
else if (partCharacter === "}") partDepth--;
|
|
44
|
+
else if (partCharacter === "," && partDepth === 0) {
|
|
45
|
+
parts.push(inside.slice(partStart, partIndex));
|
|
46
|
+
partStart = partIndex + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (parts.length === 0) return null;
|
|
50
|
+
parts.push(inside.slice(partStart));
|
|
51
|
+
return parts.map((part) => pattern.slice(0, open) + part + pattern.slice(index + 1));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function globToRegExp(pattern: string): RegExp {
|
|
58
|
+
const alternatives = braceAlternatives(pattern);
|
|
59
|
+
if (alternatives) {
|
|
60
|
+
return new RegExp(`^(?:${alternatives.map((part) => globToRegExp(part).source.slice(1, -1)).join("|")})$`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let source = "^";
|
|
64
|
+
let segmentStart = true;
|
|
65
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
66
|
+
const character = pattern[index];
|
|
67
|
+
if (character === "*") {
|
|
68
|
+
if (pattern[index + 1] === "*") {
|
|
69
|
+
index++;
|
|
70
|
+
if (pattern[index + 1] === "/") {
|
|
71
|
+
index++;
|
|
72
|
+
// Recursive wildcards do not cross dot-prefixed path segments unless
|
|
73
|
+
// the pattern explicitly starts that segment with '.'.
|
|
74
|
+
source += "(?:(?!\\.)[^/]+/)*";
|
|
75
|
+
segmentStart = true;
|
|
76
|
+
} else if (segmentStart && index + 1 === pattern.length) {
|
|
77
|
+
// A trailing globstar consumes zero or more non-hidden path
|
|
78
|
+
// segments, rather than just one basename segment.
|
|
79
|
+
source += "(?:(?!\\.)[^/]+(?:/(?!\\.)[^/]+)*)?";
|
|
80
|
+
segmentStart = false;
|
|
81
|
+
} else {
|
|
82
|
+
source += segmentStart ? "(?!\\.)[^/]*" : "[^/]*";
|
|
83
|
+
segmentStart = false;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
source += segmentStart ? "(?!\\.)[^/]*" : "[^/]*";
|
|
87
|
+
segmentStart = false;
|
|
88
|
+
}
|
|
89
|
+
} else if (character === "?") {
|
|
90
|
+
source += segmentStart ? "(?!\\.)[^/]" : "[^/]";
|
|
91
|
+
segmentStart = false;
|
|
92
|
+
} else if (character === "/") {
|
|
93
|
+
source += "/";
|
|
94
|
+
segmentStart = true;
|
|
95
|
+
} else if (character === "\\" && index + 1 < pattern.length) {
|
|
96
|
+
source += escapeRegex(pattern[++index]);
|
|
97
|
+
segmentStart = false;
|
|
98
|
+
} else {
|
|
99
|
+
source += escapeRegex(character);
|
|
100
|
+
segmentStart = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return new RegExp(`${source}$`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Match the same basename/path glob forms accepted by Pi's find and grep tools. */
|
|
107
|
+
export function matchesToolGlob(relativePath: string, pattern: string): boolean {
|
|
108
|
+
const candidate = posix(relativePath).replace(/^\.\//, "");
|
|
109
|
+
const normalizedPattern = posix(pattern).replace(/^\.\//, "");
|
|
110
|
+
if (!normalizedPattern.includes("/")) {
|
|
111
|
+
return globToRegExp(normalizedPattern).test(candidate.slice(candidate.lastIndexOf("/") + 1));
|
|
112
|
+
}
|
|
113
|
+
const patternToTest = normalizedPattern.startsWith("/")
|
|
114
|
+
? normalizedPattern.slice(1)
|
|
115
|
+
: normalizedPattern;
|
|
116
|
+
const matcher = globToRegExp(patternToTest);
|
|
117
|
+
return matcher.test(candidate) ||
|
|
118
|
+
(!patternToTest.startsWith("**/") && globToRegExp(`**/${patternToTest}`).test(candidate));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function transportErrorCode(error: unknown): string | undefined {
|
|
122
|
+
if (!error || typeof error !== "object") return undefined;
|
|
123
|
+
const code = (error as { code?: unknown }).code;
|
|
124
|
+
return typeof code === "string" ? code : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createBashOps(
|
|
128
|
+
provider: Pick<ToolOpsProvider, "withRuntime">,
|
|
129
|
+
): BashOperations {
|
|
130
|
+
return {
|
|
131
|
+
exec: async (command, cwd, { onData, signal, timeout }) => {
|
|
132
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
133
|
+
try {
|
|
134
|
+
const result = await provider.withRuntime(async ({ transport }) => {
|
|
135
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
136
|
+
return transport.execStream(BASH_COMMAND, ["-lc", command], {
|
|
137
|
+
cwd,
|
|
138
|
+
timeoutMs: timeout === undefined ? undefined : timeout * 1000,
|
|
139
|
+
signal,
|
|
140
|
+
onStdout: onData,
|
|
141
|
+
onStderr: onData,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return { exitCode: result.exitCode };
|
|
145
|
+
} catch (error) {
|
|
146
|
+
const code = transportErrorCode(error);
|
|
147
|
+
if (code === "ABORTED") throw new Error("aborted");
|
|
148
|
+
if (code === "TIMEOUT") throw new Error(`timeout:${timeout}`);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeSearchResult(raw: string, searchRoot: string): string {
|
|
156
|
+
const line = posix(raw).replace(/\r$/, "");
|
|
157
|
+
const root = posix(searchRoot).replace(/\/$/, "") || "/";
|
|
158
|
+
if (line === root) return "";
|
|
159
|
+
if (line.startsWith(`${root}/`)) return line;
|
|
160
|
+
if (line.startsWith("/")) return line;
|
|
161
|
+
const relative = line.startsWith("./") ? line.slice(2) : line;
|
|
162
|
+
return root === "/" ? `/${relative}` : `${root}/${relative}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function relativeSearchResult(absolutePath: string, searchRoot: string): string {
|
|
166
|
+
const root = posix(searchRoot).replace(/\/$/, "") || "/";
|
|
167
|
+
if (root === "/" && absolutePath.startsWith("/")) return absolutePath.slice(1);
|
|
168
|
+
if (absolutePath.startsWith(`${root}/`)) return absolutePath.slice(root.length + 1);
|
|
169
|
+
return absolutePath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createFindOps(t: SandboxTransport): FindOperations {
|
|
173
|
+
return {
|
|
174
|
+
exists: async (absolutePath) => {
|
|
175
|
+
try {
|
|
176
|
+
return await t.exists(absolutePath);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
glob: async (pattern, cwd, options) => {
|
|
182
|
+
const args = ["--files", "--hidden", "--color=never"];
|
|
183
|
+
const ignores = new Set(["**/.git/**", "**/node_modules/**", ...options.ignore]);
|
|
184
|
+
for (const ignore of ignores) {
|
|
185
|
+
const normalized = posix(ignore).replace(/^!/, "");
|
|
186
|
+
args.push("--glob", `!${normalized}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await t.exec(RG_COMMAND, args, { cwd });
|
|
190
|
+
if (result.exitCode >= 2) {
|
|
191
|
+
const message = result.stderr.toString("utf8").trim() || `rg exited with code ${result.exitCode}`;
|
|
192
|
+
throw new Error(message);
|
|
193
|
+
}
|
|
194
|
+
if (result.exitCode === 1 || result.stdout.length === 0) return [];
|
|
195
|
+
|
|
196
|
+
const paths = result.stdout
|
|
197
|
+
.toString("utf8")
|
|
198
|
+
.split("\n")
|
|
199
|
+
.filter((line) => line.length > 0)
|
|
200
|
+
.map((line) => normalizeSearchResult(line, cwd))
|
|
201
|
+
.filter((line) => line.length > 0)
|
|
202
|
+
.filter((line) => matchesToolGlob(relativeSearchResult(line, cwd), pattern));
|
|
203
|
+
return paths.slice(0, Math.max(0, options.limit));
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createGrepOps(t: SandboxTransport): GrepOperations {
|
|
209
|
+
return {
|
|
210
|
+
isDirectory: async (absolutePath) => (await t.stat(absolutePath)).kind === "directory",
|
|
211
|
+
readFile: async (absolutePath) => (await t.readFile(absolutePath)).toString("utf8"),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolvedPath(cwd: string, requested: unknown): string {
|
|
216
|
+
const value = typeof requested === "string" && requested.length > 0 ? requested : ".";
|
|
217
|
+
if (value.startsWith("/")) return posix(value).replace(/\/+/g, "/");
|
|
218
|
+
const parts = posix(cwd).split("/").filter(Boolean);
|
|
219
|
+
for (const part of posix(value).split("/")) {
|
|
220
|
+
if (!part || part === ".") continue;
|
|
221
|
+
if (part === "..") parts.pop();
|
|
222
|
+
else parts.push(part);
|
|
223
|
+
}
|
|
224
|
+
return `/${parts.join("/")}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function displayPath(searchRoot: string, rootIsDirectory: boolean, filePath: string): string {
|
|
228
|
+
const normalizedFile = posix(filePath);
|
|
229
|
+
if (!rootIsDirectory) {
|
|
230
|
+
const slash = normalizedFile.lastIndexOf("/");
|
|
231
|
+
return slash === -1 ? normalizedFile : normalizedFile.slice(slash + 1);
|
|
232
|
+
}
|
|
233
|
+
const normalizedRoot = posix(searchRoot).replace(/\/$/, "");
|
|
234
|
+
if (normalizedFile === normalizedRoot) return normalizedFile;
|
|
235
|
+
if (normalizedFile.startsWith(`${normalizedRoot}/`)) return normalizedFile.slice(normalizedRoot.length + 1);
|
|
236
|
+
return normalizedFile;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function eventFilePath(searchRoot: string, eventPath: string): string {
|
|
240
|
+
if (eventPath.startsWith("/")) return posix(eventPath);
|
|
241
|
+
return `${searchRoot.replace(/\/$/, "")}/${posix(eventPath)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isAbortError(error: unknown): boolean {
|
|
245
|
+
return transportErrorCode(error) === "ABORTED" ||
|
|
246
|
+
(error instanceof Error && /^(?:operation )?aborted$/i.test(error.message));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function createSandboxGrepExecute(args: {
|
|
250
|
+
provider: ToolOpsProvider;
|
|
251
|
+
cwd: string;
|
|
252
|
+
helpers: GrepFormattingHelpers;
|
|
253
|
+
}): SandboxGrepExecute {
|
|
254
|
+
return async (_id, params, signal, _onUpdate) => {
|
|
255
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
256
|
+
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
let externallyAborted = false;
|
|
259
|
+
const abort = () => {
|
|
260
|
+
externallyAborted = true;
|
|
261
|
+
controller.abort();
|
|
262
|
+
};
|
|
263
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
264
|
+
// An abort can happen between the initial check and listener registration.
|
|
265
|
+
if (signal?.aborted) abort();
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
return await args.provider.withRuntime(async ({ transport }) => {
|
|
269
|
+
if (controller.signal.aborted || signal?.aborted) throw new Error("Operation aborted");
|
|
270
|
+
const searchRoot = resolvedPath(args.cwd, params.path);
|
|
271
|
+
let rootIsDirectory: boolean;
|
|
272
|
+
try {
|
|
273
|
+
rootIsDirectory = (await transport.stat(searchRoot)).kind === "directory";
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (controller.signal.aborted || signal?.aborted || isAbortError(error)) {
|
|
276
|
+
throw new Error("Operation aborted");
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Path not found: ${searchRoot}`);
|
|
279
|
+
}
|
|
280
|
+
if (controller.signal.aborted || signal?.aborted) throw new Error("Operation aborted");
|
|
281
|
+
|
|
282
|
+
const pattern = typeof params.pattern === "string" ? params.pattern : "";
|
|
283
|
+
const context = typeof params.context === "number" && params.context > 0 ? params.context : 0;
|
|
284
|
+
const limit = Math.max(1, typeof params.limit === "number" ? params.limit : DEFAULT_GREP_LIMIT);
|
|
285
|
+
const rgArgs = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
286
|
+
if (params.ignoreCase) rgArgs.push("--ignore-case");
|
|
287
|
+
if (params.literal) rgArgs.push("--fixed-strings");
|
|
288
|
+
if (typeof params.glob === "string" && params.glob.length > 0) {
|
|
289
|
+
rgArgs.push("--glob", params.glob);
|
|
290
|
+
}
|
|
291
|
+
rgArgs.push("--", pattern, searchRoot);
|
|
292
|
+
|
|
293
|
+
let killedForLimit = false;
|
|
294
|
+
let matchCount = 0;
|
|
295
|
+
let stderr = "";
|
|
296
|
+
let pending = "";
|
|
297
|
+
const matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];
|
|
298
|
+
|
|
299
|
+
const parseLine = (line: string): void => {
|
|
300
|
+
if (!line.trim() || matchCount >= limit) return;
|
|
301
|
+
let event: any;
|
|
302
|
+
try {
|
|
303
|
+
event = JSON.parse(line);
|
|
304
|
+
} catch {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (event?.type !== "match") return;
|
|
308
|
+
const eventPath = event.data?.path?.text;
|
|
309
|
+
const lineNumber = event.data?.line_number;
|
|
310
|
+
if (typeof eventPath !== "string" || typeof lineNumber !== "number") return;
|
|
311
|
+
matchCount++;
|
|
312
|
+
matches.push({
|
|
313
|
+
filePath: eventFilePath(searchRoot, eventPath),
|
|
314
|
+
lineNumber,
|
|
315
|
+
lineText: typeof event.data?.lines?.text === "string" ? event.data.lines.text : undefined,
|
|
316
|
+
});
|
|
317
|
+
if (matchCount >= limit) {
|
|
318
|
+
killedForLimit = true;
|
|
319
|
+
controller.abort();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const result = await transport.execStream(RG_COMMAND, rgArgs, {
|
|
325
|
+
cwd: args.cwd,
|
|
326
|
+
signal: controller.signal,
|
|
327
|
+
onStdout: (chunk) => {
|
|
328
|
+
pending += chunk.toString("utf8");
|
|
329
|
+
const lines = pending.split("\n");
|
|
330
|
+
pending = lines.pop() ?? "";
|
|
331
|
+
for (const line of lines) parseLine(line);
|
|
332
|
+
},
|
|
333
|
+
onStderr: (chunk) => {
|
|
334
|
+
stderr += chunk.toString("utf8");
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
if (pending) parseLine(pending);
|
|
338
|
+
if (externallyAborted || signal?.aborted) throw new Error("Operation aborted");
|
|
339
|
+
if (!killedForLimit && result.exitCode >= 2) {
|
|
340
|
+
throw new Error(stderr.trim() || `ripgrep exited with code ${result.exitCode}`);
|
|
341
|
+
}
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (externallyAborted || signal?.aborted) throw new Error("Operation aborted");
|
|
344
|
+
if (!killedForLimit && isAbortError(error)) throw new Error("Operation aborted");
|
|
345
|
+
if (!killedForLimit) throw error;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (externallyAborted || signal?.aborted) throw new Error("Operation aborted");
|
|
349
|
+
if (matchCount === 0) {
|
|
350
|
+
return { content: [{ type: "text", text: "No matches found" }], details: undefined };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const outputLines: string[] = [];
|
|
354
|
+
let linesTruncated = false;
|
|
355
|
+
const fileCache = new Map<string, string[]>();
|
|
356
|
+
const getFileLines = async (filePath: string): Promise<string[]> => {
|
|
357
|
+
const cached = fileCache.get(filePath);
|
|
358
|
+
if (cached) return cached;
|
|
359
|
+
let lines: string[];
|
|
360
|
+
try {
|
|
361
|
+
const content = (await transport.readFile(filePath)).toString("utf8");
|
|
362
|
+
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (externallyAborted || signal?.aborted || isAbortError(error)) {
|
|
365
|
+
throw new Error("Operation aborted");
|
|
366
|
+
}
|
|
367
|
+
lines = [];
|
|
368
|
+
}
|
|
369
|
+
fileCache.set(filePath, lines);
|
|
370
|
+
return lines;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
for (const match of matches) {
|
|
374
|
+
if (externallyAborted || signal?.aborted) throw new Error("Operation aborted");
|
|
375
|
+
const relativePath = displayPath(searchRoot, rootIsDirectory, match.filePath);
|
|
376
|
+
if (context === 0 && match.lineText !== undefined) {
|
|
377
|
+
const lineText = match.lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
378
|
+
const truncated = args.helpers.truncateLine(lineText);
|
|
379
|
+
if (truncated.wasTruncated) linesTruncated = true;
|
|
380
|
+
outputLines.push(`${relativePath}:${match.lineNumber}: ${truncated.text}`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const lines = await getFileLines(match.filePath);
|
|
385
|
+
if (lines.length === 0) {
|
|
386
|
+
outputLines.push(`${relativePath}:${match.lineNumber}: (unable to read file)`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const start = context > 0 ? Math.max(1, match.lineNumber - context) : match.lineNumber;
|
|
390
|
+
const end = context > 0 ? Math.min(lines.length, match.lineNumber + context) : match.lineNumber;
|
|
391
|
+
for (let current = start; current <= end; current++) {
|
|
392
|
+
const raw = lines[current - 1] ?? "";
|
|
393
|
+
const truncated = args.helpers.truncateLine(raw.replace(/\r/g, ""));
|
|
394
|
+
if (truncated.wasTruncated) linesTruncated = true;
|
|
395
|
+
const separator = current === match.lineNumber ? ":" : "-";
|
|
396
|
+
outputLines.push(`${relativePath}${separator}${current}${separator} ${truncated.text}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (externallyAborted || signal?.aborted) throw new Error("Operation aborted");
|
|
401
|
+
const rawOutput = outputLines.join("\n");
|
|
402
|
+
const truncation = args.helpers.truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
403
|
+
let output = truncation.content;
|
|
404
|
+
const details: Record<string, any> = {};
|
|
405
|
+
const notices: string[] = [];
|
|
406
|
+
if (killedForLimit) {
|
|
407
|
+
details.matchLimitReached = limit;
|
|
408
|
+
notices.push(`${limit} matches limit reached. Use limit=${limit * 2} for more, or refine pattern`);
|
|
409
|
+
}
|
|
410
|
+
if (truncation.truncated) {
|
|
411
|
+
details.truncation = truncation;
|
|
412
|
+
notices.push(`${args.helpers.formatSize(args.helpers.DEFAULT_MAX_BYTES)} limit reached`);
|
|
413
|
+
}
|
|
414
|
+
if (linesTruncated) {
|
|
415
|
+
details.linesTruncated = true;
|
|
416
|
+
notices.push("Some lines truncated to 500 chars. Use read tool to see full lines");
|
|
417
|
+
}
|
|
418
|
+
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
content: [{ type: "text", text: output }],
|
|
422
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (externallyAborted || signal?.aborted || isAbortError(error)) {
|
|
427
|
+
throw new Error("Operation aborted");
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
} finally {
|
|
431
|
+
signal?.removeEventListener("abort", abort);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|