@mariozechner/pi-coding-agent 0.13.1 → 0.13.2
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 +12 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +88 -50
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +49 -28
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +37 -9
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +28 -10
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +52 -31
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/truncate.d.ts +66 -0
- package/dist/tools/truncate.d.ts.map +1 -0
- package/dist/tools/truncate.js +195 -0
- package/dist/tools/truncate.js.map +1 -0
- package/dist/tui/tool-execution.d.ts.map +1 -1
- package/dist/tui/tool-execution.js +81 -5
- package/dist/tui/tool-execution.js.map +1 -1
- package/package.json +4 -4
package/dist/tools/grep.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFileSync, statSync } from "fs";
|
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { ensureTool } from "../tools-manager.js";
|
|
8
|
+
import { DEFAULT_MAX_BYTES, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine, } from "./truncate.js";
|
|
8
9
|
/**
|
|
9
10
|
* Expand ~ to home directory
|
|
10
11
|
*/
|
|
@@ -30,7 +31,7 @@ const DEFAULT_LIMIT = 100;
|
|
|
30
31
|
export const grepTool = {
|
|
31
32
|
name: "grep",
|
|
32
33
|
label: "grep",
|
|
33
|
-
description:
|
|
34
|
+
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
|
34
35
|
parameters: grepSchema,
|
|
35
36
|
execute: async (_toolCallId, { pattern, path: searchDir, glob, ignoreCase, literal, context, limit, }, signal) => {
|
|
36
37
|
return new Promise((resolve, reject) => {
|
|
@@ -103,7 +104,8 @@ export const grepTool = {
|
|
|
103
104
|
const rl = createInterface({ input: child.stdout });
|
|
104
105
|
let stderr = "";
|
|
105
106
|
let matchCount = 0;
|
|
106
|
-
let
|
|
107
|
+
let matchLimitReached = false;
|
|
108
|
+
let linesTruncated = false;
|
|
107
109
|
let aborted = false;
|
|
108
110
|
let killedDueToLimit = false;
|
|
109
111
|
const outputLines = [];
|
|
@@ -138,11 +140,16 @@ export const grepTool = {
|
|
|
138
140
|
const lineText = lines[current - 1] ?? "";
|
|
139
141
|
const sanitized = lineText.replace(/\r/g, "");
|
|
140
142
|
const isMatchLine = current === lineNumber;
|
|
143
|
+
// Truncate long lines
|
|
144
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
145
|
+
if (wasTruncated) {
|
|
146
|
+
linesTruncated = true;
|
|
147
|
+
}
|
|
141
148
|
if (isMatchLine) {
|
|
142
|
-
block.push(`${relativePath}:${current}: ${
|
|
149
|
+
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
143
150
|
}
|
|
144
151
|
else {
|
|
145
|
-
block.push(`${relativePath}-${current}- ${
|
|
152
|
+
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
return block;
|
|
@@ -166,7 +173,7 @@ export const grepTool = {
|
|
|
166
173
|
outputLines.push(...formatBlock(filePath, lineNumber));
|
|
167
174
|
}
|
|
168
175
|
if (matchCount >= effectiveLimit) {
|
|
169
|
-
|
|
176
|
+
matchLimitReached = true;
|
|
170
177
|
stopChild(true);
|
|
171
178
|
}
|
|
172
179
|
}
|
|
@@ -190,11 +197,32 @@ export const grepTool = {
|
|
|
190
197
|
settle(() => resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }));
|
|
191
198
|
return;
|
|
192
199
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
// Apply byte truncation (no line limit since we already have match limit)
|
|
201
|
+
const rawOutput = outputLines.join("\n");
|
|
202
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
203
|
+
let output = truncation.content;
|
|
204
|
+
const details = {};
|
|
205
|
+
// Build notices
|
|
206
|
+
const notices = [];
|
|
207
|
+
if (matchLimitReached) {
|
|
208
|
+
notices.push(`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
|
|
209
|
+
details.matchLimitReached = effectiveLimit;
|
|
210
|
+
}
|
|
211
|
+
if (truncation.truncated) {
|
|
212
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
213
|
+
details.truncation = truncation;
|
|
214
|
+
}
|
|
215
|
+
if (linesTruncated) {
|
|
216
|
+
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
217
|
+
details.linesTruncated = true;
|
|
218
|
+
}
|
|
219
|
+
if (notices.length > 0) {
|
|
220
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
196
221
|
}
|
|
197
|
-
settle(() => resolve({
|
|
222
|
+
settle(() => resolve({
|
|
223
|
+
content: [{ type: "text", text: output }],
|
|
224
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
225
|
+
}));
|
|
198
226
|
});
|
|
199
227
|
}
|
|
200
228
|
catch (err) {
|
package/dist/tools/grep.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grep.js","sourceRoot":"","sources":["../../src/tools/grep.ts"],"names":[],"mappings":"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","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, type Stats, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport path from \"path\";\nimport { ensureTool } from \"../tools-manager.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst DEFAULT_LIMIT = 100;\n\nexport const grepTool: AgentTool<typeof grepSchema> = {\n\tname: \"grep\",\n\tlabel: \"grep\",\n\tdescription:\n\t\t\"Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore.\",\n\tparameters: grepSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{\n\t\t\tpattern,\n\t\t\tpath: searchDir,\n\t\t\tglob,\n\t\t\tignoreCase,\n\t\t\tliteral,\n\t\t\tcontext,\n\t\t\tlimit,\n\t\t}: {\n\t\t\tpattern: string;\n\t\t\tpath?: string;\n\t\t\tglob?: string;\n\t\t\tignoreCase?: boolean;\n\t\t\tliteral?: boolean;\n\t\t\tcontext?: number;\n\t\t\tlimit?: number;\n\t\t},\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tfn();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst searchPath = path.resolve(expandPath(searchDir || \".\"));\n\t\t\t\t\tlet searchStat: Stats;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsearchStat = statSync(searchPath);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isDirectory = searchStat.isDirectory();\n\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\n\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\tconst getFileLines = (filePath: string): string[] => {\n\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn lines;\n\t\t\t\t\t};\n\n\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\n\t\t\t\t\tif (ignoreCase) {\n\t\t\t\t\t\targs.push(\"--ignore-case\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (literal) {\n\t\t\t\t\t\targs.push(\"--fixed-strings\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (glob) {\n\t\t\t\t\t\targs.push(\"--glob\", glob);\n\t\t\t\t\t}\n\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\tlet truncated = false;\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\trl.close();\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst stopChild = (dueToLimit: boolean = false) => {\n\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\tstopChild();\n\t\t\t\t\t};\n\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t});\n\n\t\t\t\t\tconst formatBlock = (filePath: string, lineNumber: number) => {\n\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\tconst lines = getFileLines(filePath);\n\t\t\t\t\t\tif (!lines.length) {\n\t\t\t\t\t\t\treturn [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\n\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\n\t\t\t\t\t\t\tif (isMatchLine) {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}:${current}: ${sanitized}`);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}-${current}- ${sanitized}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn block;\n\t\t\t\t\t};\n\n\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\n\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\") {\n\t\t\t\t\t\t\t\toutputLines.push(...formatBlock(filePath, lineNumber));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\ttruncated = true;\n\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\t\tcleanup();\n\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet output = outputLines.join(\"\\n\");\n\t\t\t\t\t\tif (truncated) {\n\t\t\t\t\t\t\toutput += `\\n\\n(truncated, limit of ${effectiveLimit} matches reached)`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsettle(() => resolve({ content: [{ type: \"text\", text: output }], details: undefined }));\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"grep.js","sourceRoot":"","sources":["../../src/tools/grep.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,YAAY,EAAc,QAAQ,EAAE,MAAM,IAAI,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACN,iBAAiB,EACjB,UAAU,EACV,oBAAoB,EAEpB,YAAY,EACZ,YAAY,GACZ,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,SAAS,UAAU,CAAC,QAAgB,EAAU;IAC7C,IAAI,QAAQ,KAAK,GAAG,EAAE,CAAC;QACtB,OAAO,OAAO,EAAE,CAAC;IAClB,CAAC;IACD,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,OAAO,OAAO,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,QAAQ,CAAC;AAAA,CAChB;AAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;IACjF,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC,CAAC;IAC7G,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC,CAAC;IAChH,UAAU,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC,CAAC;IACpG,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,mEAAmE,EAAE,CAAC,CAClG;IACD,OAAO,EAAE,IAAI,CAAC,QAAQ,CACrB,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kEAAkE,EAAE,CAAC,CAChG;IACD,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,oDAAoD,EAAE,CAAC,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,GAAG,CAAC;AAQ1B,MAAM,CAAC,MAAM,QAAQ,GAAiC;IACrD,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,WAAW,EAAE,4IAA4I,aAAa,eAAe,iBAAiB,GAAG,IAAI,4DAA4D,oBAAoB,SAAS;IACtS,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EACC,OAAO,EACP,IAAI,EAAE,SAAS,EACf,IAAI,EACJ,UAAU,EACV,OAAO,EACP,OAAO,EACP,KAAK,GASL,EACD,MAAoB,EACnB,EAAE,CAAC;QACJ,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;gBACvC,OAAO;YACR,CAAC;YAED,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,MAAM,MAAM,GAAG,CAAC,EAAc,EAAE,EAAE,CAAC;gBAClC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACd,OAAO,GAAG,IAAI,CAAC;oBACf,EAAE,EAAE,CAAC;gBACN,CAAC;YAAA,CACD,CAAC;YAEF,CAAC,KAAK,IAAI,EAAE,CAAC;gBACZ,IAAI,CAAC;oBACJ,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;oBAC5C,IAAI,CAAC,MAAM,EAAE,CAAC;wBACb,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC,CAAC,CAAC;wBAC7F,OAAO;oBACR,CAAC;oBAED,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,CAAC;oBAC9D,IAAI,UAAiB,CAAC;oBACtB,IAAI,CAAC;wBACJ,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC;oBACnC,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACd,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;wBACjE,OAAO;oBACR,CAAC;oBAED,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;oBAC7C,MAAM,YAAY,GAAG,OAAO,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;oBAC1D,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,IAAI,aAAa,CAAC,CAAC;oBAE3D,MAAM,UAAU,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC;wBAChD,IAAI,WAAW,EAAE,CAAC;4BACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;4BACrD,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gCAC5C,OAAO,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;4BACrC,CAAC;wBACF,CAAC;wBACD,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;oBAAA,CAC/B,CAAC;oBAEF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAoB,CAAC;oBAC9C,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAY,EAAE,CAAC;wBACpD,IAAI,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;wBACpC,IAAI,CAAC,KAAK,EAAE,CAAC;4BACZ,IAAI,CAAC;gCACJ,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gCAChD,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;4BACzE,CAAC;4BAAC,MAAM,CAAC;gCACR,KAAK,GAAG,EAAE,CAAC;4BACZ,CAAC;4BACD,SAAS,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;wBAChC,CAAC;wBACD,OAAO,KAAK,CAAC;oBAAA,CACb,CAAC;oBAEF,MAAM,IAAI,GAAa,CAAC,QAAQ,EAAE,eAAe,EAAE,eAAe,EAAE,UAAU,CAAC,CAAC;oBAEhF,IAAI,UAAU,EAAE,CAAC;wBAChB,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;oBAC5B,CAAC;oBAED,IAAI,OAAO,EAAE,CAAC;wBACb,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;oBAC9B,CAAC;oBAED,IAAI,IAAI,EAAE,CAAC;wBACV,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;oBAC3B,CAAC;oBAED,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;oBAE/B,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;oBACzE,MAAM,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;oBACpD,IAAI,MAAM,GAAG,EAAE,CAAC;oBAChB,IAAI,UAAU,GAAG,CAAC,CAAC;oBACnB,IAAI,iBAAiB,GAAG,KAAK,CAAC;oBAC9B,IAAI,cAAc,GAAG,KAAK,CAAC;oBAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;oBACpB,IAAI,gBAAgB,GAAG,KAAK,CAAC;oBAC7B,MAAM,WAAW,GAAa,EAAE,CAAC;oBAEjC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;wBACrB,EAAE,CAAC,KAAK,EAAE,CAAC;wBACX,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAAA,CAC9C,CAAC;oBAEF,MAAM,SAAS,GAAG,CAAC,UAAU,GAAY,KAAK,EAAE,EAAE,CAAC;wBAClD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;4BACnB,gBAAgB,GAAG,UAAU,CAAC;4BAC9B,KAAK,CAAC,IAAI,EAAE,CAAC;wBACd,CAAC;oBAAA,CACD,CAAC;oBAEF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;wBACrB,OAAO,GAAG,IAAI,CAAC;wBACf,SAAS,EAAE,CAAC;oBAAA,CACZ,CAAC;oBAEF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;oBAE3D,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;wBACnC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;oBAAA,CAC3B,CAAC,CAAC;oBAEH,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAE,UAAkB,EAAY,EAAE,CAAC;wBACvE,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;wBAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;wBACrC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;4BACnB,OAAO,CAAC,GAAG,YAAY,IAAI,UAAU,yBAAyB,CAAC,CAAC;wBACjE,CAAC;wBAED,MAAM,KAAK,GAAa,EAAE,CAAC;wBAC3B,MAAM,KAAK,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;wBACrF,MAAM,GAAG,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;wBAE9F,KAAK,IAAI,OAAO,GAAG,KAAK,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC;4BACrD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;4BAC1C,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;4BAC9C,MAAM,WAAW,GAAG,OAAO,KAAK,UAAU,CAAC;4BAE3C,sBAAsB;4BACtB,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;4BACtE,IAAI,YAAY,EAAE,CAAC;gCAClB,cAAc,GAAG,IAAI,CAAC;4BACvB,CAAC;4BAED,IAAI,WAAW,EAAE,CAAC;gCACjB,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC,CAAC;4BAC5D,CAAC;iCAAM,CAAC;gCACP,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC,CAAC;4BAC5D,CAAC;wBACF,CAAC;wBAED,OAAO,KAAK,CAAC;oBAAA,CACb,CAAC;oBAEF,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;wBACvB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,UAAU,IAAI,cAAc,EAAE,CAAC;4BAClD,OAAO;wBACR,CAAC;wBAED,IAAI,KAAU,CAAC;wBACf,IAAI,CAAC;4BACJ,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAC1B,CAAC;wBAAC,MAAM,CAAC;4BACR,OAAO;wBACR,CAAC;wBAED,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;4BAC5B,UAAU,EAAE,CAAC;4BACb,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;4BACxC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC;4BAE3C,IAAI,QAAQ,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;gCAChD,WAAW,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;4BACxD,CAAC;4BAED,IAAI,UAAU,IAAI,cAAc,EAAE,CAAC;gCAClC,iBAAiB,GAAG,IAAI,CAAC;gCACzB,SAAS,CAAC,IAAI,CAAC,CAAC;4BACjB,CAAC;wBACF,CAAC;oBAAA,CACD,CAAC,CAAC;oBAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;wBAC5B,OAAO,EAAE,CAAC;wBACV,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;oBAAA,CAC3E,CAAC,CAAC;oBAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;wBAC3B,OAAO,EAAE,CAAC;wBAEV,IAAI,OAAO,EAAE,CAAC;4BACb,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;4BACrD,OAAO;wBACR,CAAC;wBAED,IAAI,CAAC,gBAAgB,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;4BACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,EAAE,IAAI,4BAA4B,IAAI,EAAE,CAAC;4BACrE,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;4BAC1C,OAAO;wBACR,CAAC;wBAED,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;4BACtB,MAAM,CAAC,GAAG,EAAE,CACX,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CACtF,CAAC;4BACF,OAAO;wBACR,CAAC;wBAED,0EAA0E;wBAC1E,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBACzC,MAAM,UAAU,GAAG,YAAY,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;wBAElF,IAAI,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC;wBAChC,MAAM,OAAO,GAAoB,EAAE,CAAC;wBAEpC,gBAAgB;wBAChB,MAAM,OAAO,GAAa,EAAE,CAAC;wBAE7B,IAAI,iBAAiB,EAAE,CAAC;4BACvB,OAAO,CAAC,IAAI,CACX,GAAG,cAAc,qCAAqC,cAAc,GAAG,CAAC,8BAA8B,CACtG,CAAC;4BACF,OAAO,CAAC,iBAAiB,GAAG,cAAc,CAAC;wBAC5C,CAAC;wBAED,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;4BAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;4BAC/D,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC;wBACjC,CAAC;wBAED,IAAI,cAAc,EAAE,CAAC;4BACpB,OAAO,CAAC,IAAI,CACX,2BAA2B,oBAAoB,yCAAyC,CACxF,CAAC;4BACF,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;wBAC/B,CAAC;wBAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BACxB,MAAM,IAAI,QAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;wBACzC,CAAC;wBAED,MAAM,CAAC,GAAG,EAAE,CACX,OAAO,CAAC;4BACP,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4BACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;yBAC9D,CAAC,CACF,CAAC;oBAAA,CACF,CAAC,CAAC;gBACJ,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAY,CAAC,CAAC,CAAC;gBACpC,CAAC;YAAA,CACD,CAAC,EAAE,CAAC;QAAA,CACL,CAAC,CAAC;IAAA,CACH;CACD,CAAC","sourcesContent":["import { createInterface } from \"node:readline\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { readFileSync, type Stats, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport path from \"path\";\nimport { ensureTool } from \"../tools-manager.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tformatSize,\n\tGREP_MAX_LINE_LENGTH,\n\ttype TruncationResult,\n\ttruncateHead,\n\ttruncateLine,\n} from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tcontext: Type.Optional(\n\t\tType.Number({ description: \"Number of lines to show before and after each match (default: 0)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst DEFAULT_LIMIT = 100;\n\ninterface GrepToolDetails {\n\ttruncation?: TruncationResult;\n\tmatchLimitReached?: number;\n\tlinesTruncated?: boolean;\n}\n\nexport const grepTool: AgentTool<typeof grepSchema> = {\n\tname: \"grep\",\n\tlabel: \"grep\",\n\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,\n\tparameters: grepSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{\n\t\t\tpattern,\n\t\t\tpath: searchDir,\n\t\t\tglob,\n\t\t\tignoreCase,\n\t\t\tliteral,\n\t\t\tcontext,\n\t\t\tlimit,\n\t\t}: {\n\t\t\tpattern: string;\n\t\t\tpath?: string;\n\t\t\tglob?: string;\n\t\t\tignoreCase?: boolean;\n\t\t\tliteral?: boolean;\n\t\t\tcontext?: number;\n\t\t\tlimit?: number;\n\t\t},\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (fn: () => void) => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tfn();\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst rgPath = await ensureTool(\"rg\", true);\n\t\t\t\t\tif (!rgPath) {\n\t\t\t\t\t\tsettle(() => reject(new Error(\"ripgrep (rg) is not available and could not be downloaded\")));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst searchPath = path.resolve(expandPath(searchDir || \".\"));\n\t\t\t\t\tlet searchStat: Stats;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsearchStat = statSync(searchPath);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tsettle(() => reject(new Error(`Path not found: ${searchPath}`)));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst isDirectory = searchStat.isDirectory();\n\t\t\t\t\tconst contextValue = context && context > 0 ? context : 0;\n\t\t\t\t\tconst effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);\n\n\t\t\t\t\tconst formatPath = (filePath: string): string => {\n\t\t\t\t\t\tif (isDirectory) {\n\t\t\t\t\t\t\tconst relative = path.relative(searchPath, filePath);\n\t\t\t\t\t\t\tif (relative && !relative.startsWith(\"..\")) {\n\t\t\t\t\t\t\t\treturn relative.replace(/\\\\/g, \"/\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn path.basename(filePath);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst fileCache = new Map<string, string[]>();\n\t\t\t\t\tconst getFileLines = (filePath: string): string[] => {\n\t\t\t\t\t\tlet lines = fileCache.get(filePath);\n\t\t\t\t\t\tif (!lines) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst content = readFileSync(filePath, \"utf-8\");\n\t\t\t\t\t\t\t\tlines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\tlines = [];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfileCache.set(filePath, lines);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn lines;\n\t\t\t\t\t};\n\n\t\t\t\t\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\n\t\t\t\t\tif (ignoreCase) {\n\t\t\t\t\t\targs.push(\"--ignore-case\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (literal) {\n\t\t\t\t\t\targs.push(\"--fixed-strings\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (glob) {\n\t\t\t\t\t\targs.push(\"--glob\", glob);\n\t\t\t\t\t}\n\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\tconst child = spawn(rgPath, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\t\t\t\t\tconst rl = createInterface({ input: child.stdout });\n\t\t\t\t\tlet stderr = \"\";\n\t\t\t\t\tlet matchCount = 0;\n\t\t\t\t\tlet matchLimitReached = false;\n\t\t\t\t\tlet linesTruncated = false;\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tlet killedDueToLimit = false;\n\t\t\t\t\tconst outputLines: string[] = [];\n\n\t\t\t\t\tconst cleanup = () => {\n\t\t\t\t\t\trl.close();\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t};\n\n\t\t\t\t\tconst stopChild = (dueToLimit: boolean = false) => {\n\t\t\t\t\t\tif (!child.killed) {\n\t\t\t\t\t\t\tkilledDueToLimit = dueToLimit;\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\tstopChild();\n\t\t\t\t\t};\n\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\tchild.stderr?.on(\"data\", (chunk) => {\n\t\t\t\t\t\tstderr += chunk.toString();\n\t\t\t\t\t});\n\n\t\t\t\t\tconst formatBlock = (filePath: string, lineNumber: number): string[] => {\n\t\t\t\t\t\tconst relativePath = formatPath(filePath);\n\t\t\t\t\t\tconst lines = getFileLines(filePath);\n\t\t\t\t\t\tif (!lines.length) {\n\t\t\t\t\t\t\treturn [`${relativePath}:${lineNumber}: (unable to read file)`];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst block: string[] = [];\n\t\t\t\t\t\tconst start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;\n\t\t\t\t\t\tconst end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;\n\n\t\t\t\t\t\tfor (let current = start; current <= end; current++) {\n\t\t\t\t\t\t\tconst lineText = lines[current - 1] ?? \"\";\n\t\t\t\t\t\t\tconst sanitized = lineText.replace(/\\r/g, \"\");\n\t\t\t\t\t\t\tconst isMatchLine = current === lineNumber;\n\n\t\t\t\t\t\t\t// Truncate long lines\n\t\t\t\t\t\t\tconst { text: truncatedText, wasTruncated } = truncateLine(sanitized);\n\t\t\t\t\t\t\tif (wasTruncated) {\n\t\t\t\t\t\t\t\tlinesTruncated = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isMatchLine) {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}:${current}: ${truncatedText}`);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tblock.push(`${relativePath}-${current}- ${truncatedText}`);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn block;\n\t\t\t\t\t};\n\n\t\t\t\t\trl.on(\"line\", (line) => {\n\t\t\t\t\t\tif (!line.trim() || matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tlet event: any;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tevent = JSON.parse(line);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (event.type === \"match\") {\n\t\t\t\t\t\t\tmatchCount++;\n\t\t\t\t\t\t\tconst filePath = event.data?.path?.text;\n\t\t\t\t\t\t\tconst lineNumber = event.data?.line_number;\n\n\t\t\t\t\t\t\tif (filePath && typeof lineNumber === \"number\") {\n\t\t\t\t\t\t\t\toutputLines.push(...formatBlock(filePath, lineNumber));\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (matchCount >= effectiveLimit) {\n\t\t\t\t\t\t\t\tmatchLimitReached = true;\n\t\t\t\t\t\t\t\tstopChild(true);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"error\", (error) => {\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tsettle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));\n\t\t\t\t\t});\n\n\t\t\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\t\t\tcleanup();\n\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\tsettle(() => reject(new Error(\"Operation aborted\")));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!killedDueToLimit && code !== 0 && code !== 1) {\n\t\t\t\t\t\t\tconst errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;\n\t\t\t\t\t\t\tsettle(() => reject(new Error(errorMsg)));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (matchCount === 0) {\n\t\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"No matches found\" }], details: undefined }),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Apply byte truncation (no line limit since we already have match limit)\n\t\t\t\t\t\tconst rawOutput = outputLines.join(\"\\n\");\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: GrepToolDetails = {};\n\n\t\t\t\t\t\t// Build notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (matchLimitReached) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.matchLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (linesTruncated) {\n\t\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t\t`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tdetails.linesTruncated = true;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsettle(() =>\n\t\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t});\n\t\t\t\t} catch (err) {\n\t\t\t\t\tsettle(() => reject(err as Error));\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
package/dist/tools/ls.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../src/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../src/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAoBrD,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AASH,eAAO,MAAM,MAAM,EAAE,SAAS,CAAC,OAAO,QAAQ,CA+G7C,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport nodePath from \"path\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\ninterface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport const lsTool: AgentTool<typeof lsSchema> = {\n\tname: \"ls\",\n\tlabel: \"ls\",\n\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\tparameters: lsSchema,\n\texecute: async (_toolCallId: string, { path, limit }: { path?: string; limit?: number }, signal?: AbortSignal) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tconst dirPath = nodePath.resolve(expandPath(path || \".\"));\n\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t// Check if path exists\n\t\t\t\tif (!existsSync(dirPath)) {\n\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Check if path is a directory\n\t\t\t\tconst stat = statSync(dirPath);\n\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Read directory entries\n\t\t\t\tlet entries: string[];\n\t\t\t\ttry {\n\t\t\t\t\tentries = readdirSync(dirPath);\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t// Format entries with directory indicators\n\t\t\t\tconst results: string[] = [];\n\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst entryStat = statSync(fullPath);\n\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t}\n\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\tif (results.length === 0) {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\tlet output = truncation.content;\n\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t// Build notices\n\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t}\n\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t}\n\n\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t}\n\n\t\t\t\tresolve({\n\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t});\n\t\t\t} catch (e: any) {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(e);\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
package/dist/tools/ls.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
2
2
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import nodePath from "path";
|
|
5
|
+
import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
|
|
5
6
|
/**
|
|
6
7
|
* Expand ~ to home directory
|
|
7
8
|
*/
|
|
@@ -22,7 +23,7 @@ const DEFAULT_LIMIT = 500;
|
|
|
22
23
|
export const lsTool = {
|
|
23
24
|
name: "ls",
|
|
24
25
|
label: "ls",
|
|
25
|
-
description:
|
|
26
|
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
|
26
27
|
parameters: lsSchema,
|
|
27
28
|
execute: async (_toolCallId, { path, limit }, signal) => {
|
|
28
29
|
return new Promise((resolve, reject) => {
|
|
@@ -59,10 +60,10 @@ export const lsTool = {
|
|
|
59
60
|
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
60
61
|
// Format entries with directory indicators
|
|
61
62
|
const results = [];
|
|
62
|
-
let
|
|
63
|
+
let entryLimitReached = false;
|
|
63
64
|
for (const entry of entries) {
|
|
64
65
|
if (results.length >= effectiveLimit) {
|
|
65
|
-
|
|
66
|
+
entryLimitReached = true;
|
|
66
67
|
break;
|
|
67
68
|
}
|
|
68
69
|
const fullPath = nodePath.join(dirPath, entry);
|
|
@@ -80,15 +81,32 @@ export const lsTool = {
|
|
|
80
81
|
results.push(entry + suffix);
|
|
81
82
|
}
|
|
82
83
|
signal?.removeEventListener("abort", onAbort);
|
|
83
|
-
let output = results.join("\n");
|
|
84
|
-
if (truncated) {
|
|
85
|
-
const remaining = entries.length - effectiveLimit;
|
|
86
|
-
output += `\n\n(truncated, ${remaining} more entries)`;
|
|
87
|
-
}
|
|
88
84
|
if (results.length === 0) {
|
|
89
|
-
|
|
85
|
+
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Apply byte truncation (no line limit since we already have entry limit)
|
|
89
|
+
const rawOutput = results.join("\n");
|
|
90
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
91
|
+
let output = truncation.content;
|
|
92
|
+
const details = {};
|
|
93
|
+
// Build notices
|
|
94
|
+
const notices = [];
|
|
95
|
+
if (entryLimitReached) {
|
|
96
|
+
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
|
|
97
|
+
details.entryLimitReached = effectiveLimit;
|
|
98
|
+
}
|
|
99
|
+
if (truncation.truncated) {
|
|
100
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
101
|
+
details.truncation = truncation;
|
|
102
|
+
}
|
|
103
|
+
if (notices.length > 0) {
|
|
104
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
90
105
|
}
|
|
91
|
-
resolve({
|
|
106
|
+
resolve({
|
|
107
|
+
content: [{ type: "text", text: output }],
|
|
108
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
109
|
+
});
|
|
92
110
|
}
|
|
93
111
|
catch (e) {
|
|
94
112
|
signal?.removeEventListener("abort", onAbort);
|
package/dist/tools/ls.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.js","sourceRoot":"","sources":["../../src/tools/ls.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,QAAQ,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ls.js","sourceRoot":"","sources":["../../src/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport nodePath from \"path\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst DEFAULT_LIMIT = 500;\n\ninterface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport const lsTool: AgentTool<typeof lsSchema> = {\n\tname: \"ls\",\n\tlabel: \"ls\",\n\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\tparameters: lsSchema,\n\texecute: async (_toolCallId: string, { path, limit }: { path?: string; limit?: number }, signal?: AbortSignal) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\ttry {\n\t\t\t\tconst dirPath = nodePath.resolve(expandPath(path || \".\"));\n\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t// Check if path exists\n\t\t\t\tif (!existsSync(dirPath)) {\n\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Check if path is a directory\n\t\t\t\tconst stat = statSync(dirPath);\n\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Read directory entries\n\t\t\t\tlet entries: string[];\n\t\t\t\ttry {\n\t\t\t\t\tentries = readdirSync(dirPath);\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Sort alphabetically (case-insensitive)\n\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t// Format entries with directory indicators\n\t\t\t\tconst results: string[] = [];\n\t\t\t\tlet entryLimitReached = false;\n\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\tlet suffix = \"\";\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst entryStat = statSync(fullPath);\n\t\t\t\t\t\tif (entryStat.isDirectory()) {\n\t\t\t\t\t\t\tsuffix = \"/\";\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Skip entries we can't stat\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t}\n\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\tif (results.length === 0) {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Apply byte truncation (no line limit since we already have entry limit)\n\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\tlet output = truncation.content;\n\t\t\t\tconst details: LsToolDetails = {};\n\n\t\t\t\t// Build notices\n\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t}\n\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t}\n\n\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t}\n\n\t\t\t\tresolve({\n\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t});\n\t\t\t} catch (e: any) {\n\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\treject(e);\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
package/dist/tools/read.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAA6B,MAAM,qBAAqB,CAAC;AAuChF,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAMH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAmJjD,CAAC","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { extname, resolve as resolvePath } from \"path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\tparameters: readSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t);\n\t},\n};\n"]}
|
package/dist/tools/read.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
import { constants } from "fs";
|
|
4
4
|
import { access, readFile } from "fs/promises";
|
|
5
5
|
import { extname, resolve as resolvePath } from "path";
|
|
6
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
|
|
6
7
|
/**
|
|
7
8
|
* Expand ~ to home directory
|
|
8
9
|
*/
|
|
@@ -37,12 +38,10 @@ const readSchema = Type.Object({
|
|
|
37
38
|
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
38
39
|
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
39
40
|
});
|
|
40
|
-
const MAX_LINES = 2000;
|
|
41
|
-
const MAX_LINE_LENGTH = 2000;
|
|
42
41
|
export const readTool = {
|
|
43
42
|
name: "read",
|
|
44
43
|
label: "read",
|
|
45
|
-
description:
|
|
44
|
+
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,
|
|
46
45
|
parameters: readSchema,
|
|
47
46
|
execute: async (_toolCallId, { path, offset, limit }, signal) => {
|
|
48
47
|
const absolutePath = resolvePath(expandPath(path));
|
|
@@ -73,6 +72,7 @@ export const readTool = {
|
|
|
73
72
|
}
|
|
74
73
|
// Read the file based on type
|
|
75
74
|
let content;
|
|
75
|
+
let details;
|
|
76
76
|
if (mimeType) {
|
|
77
77
|
// Read as image (binary)
|
|
78
78
|
const buffer = await readFile(absolutePath);
|
|
@@ -85,38 +85,59 @@ export const readTool = {
|
|
|
85
85
|
else {
|
|
86
86
|
// Read as text
|
|
87
87
|
const textContent = await readFile(absolutePath, "utf-8");
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
const
|
|
88
|
+
const allLines = textContent.split("\n");
|
|
89
|
+
const totalFileLines = allLines.length;
|
|
90
|
+
// Apply offset if specified (1-indexed to 0-indexed)
|
|
91
|
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
|
92
|
+
const startLineDisplay = startLine + 1; // For display (1-indexed)
|
|
93
93
|
// Check if offset is out of bounds
|
|
94
|
-
if (startLine >=
|
|
95
|
-
throw new Error(`Offset ${offset} is beyond end of file (${
|
|
94
|
+
if (startLine >= allLines.length) {
|
|
95
|
+
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
|
96
96
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
// If limit is specified by user, use it; otherwise we'll let truncateHead decide
|
|
98
|
+
let selectedContent;
|
|
99
|
+
let userLimitedLines;
|
|
100
|
+
if (limit !== undefined) {
|
|
101
|
+
const endLine = Math.min(startLine + limit, allLines.length);
|
|
102
|
+
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
103
|
+
userLimitedLines = endLine - startLine;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
selectedContent = allLines.slice(startLine).join("\n");
|
|
107
|
+
}
|
|
108
|
+
// Apply truncation (respects both line and byte limits)
|
|
109
|
+
const truncation = truncateHead(selectedContent);
|
|
110
|
+
let outputText;
|
|
111
|
+
if (truncation.firstLineExceedsLimit) {
|
|
112
|
+
// First line at offset exceeds 30KB - tell model to use bash
|
|
113
|
+
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
|
|
114
|
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
115
|
+
details = { truncation };
|
|
116
|
+
}
|
|
117
|
+
else if (truncation.truncated) {
|
|
118
|
+
// Truncation occurred - build actionable notice
|
|
119
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
120
|
+
const nextOffset = endLineDisplay + 1;
|
|
121
|
+
outputText = truncation.content;
|
|
122
|
+
if (truncation.truncatedBy === "lines") {
|
|
123
|
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;
|
|
105
127
|
}
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
let outputText = formattedLines.join("\n");
|
|
109
|
-
// Add notices
|
|
110
|
-
const notices = [];
|
|
111
|
-
if (hadTruncatedLines) {
|
|
112
|
-
notices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);
|
|
128
|
+
details = { truncation };
|
|
113
129
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
|
131
|
+
// User specified limit, there's more content, but no truncation
|
|
132
|
+
const endLineDisplay = startLineDisplay + userLimitedLines - 1;
|
|
133
|
+
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
134
|
+
const nextOffset = startLine + userLimitedLines + 1;
|
|
135
|
+
outputText = truncation.content;
|
|
136
|
+
outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
|
|
117
137
|
}
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
else {
|
|
139
|
+
// No truncation, no user limit exceeded
|
|
140
|
+
outputText = truncation.content;
|
|
120
141
|
}
|
|
121
142
|
content = [{ type: "text", text: outputText }];
|
|
122
143
|
}
|
|
@@ -128,7 +149,7 @@ export const readTool = {
|
|
|
128
149
|
if (signal) {
|
|
129
150
|
signal.removeEventListener("abort", onAbort);
|
|
130
151
|
}
|
|
131
|
-
resolve({ content, details
|
|
152
|
+
resolve({ content, details });
|
|
132
153
|
}
|
|
133
154
|
catch (error) {
|
|
134
155
|
// Clean up abort handler
|
package/dist/tools/read.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { extname, resolve as resolvePath } from \"path\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nconst MAX_LINES = 2000;\nconst MAX_LINE_LENGTH = 2000;\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription:\n\t\t\"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit for large files.\",\n\tparameters: readSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: undefined }>((resolve, reject) => {\n\t\t\t// Check if already aborted\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet aborted = false;\n\n\t\t\t// Set up abort handler\n\t\t\tconst onAbort = () => {\n\t\t\t\taborted = true;\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\n\t\t\t// Perform the read operation\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Check if file exists\n\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Read the file based on type\n\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\n\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\tconst lines = textContent.split(\"\\n\");\n\n\t\t\t\t\t\t// Apply offset and limit (matching Claude Code Read tool behavior)\n\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0; // 1-indexed to 0-indexed\n\t\t\t\t\t\tconst maxLines = limit || MAX_LINES;\n\t\t\t\t\t\tconst endLine = Math.min(startLine + maxLines, lines.length);\n\n\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\tif (startLine >= lines.length) {\n\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${lines.length} lines total)`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Get the relevant lines\n\t\t\t\t\t\tconst selectedLines = lines.slice(startLine, endLine);\n\n\t\t\t\t\t\t// Truncate long lines and track which were truncated\n\t\t\t\t\t\tlet hadTruncatedLines = false;\n\t\t\t\t\t\tconst formattedLines = selectedLines.map((line) => {\n\t\t\t\t\t\t\tif (line.length > MAX_LINE_LENGTH) {\n\t\t\t\t\t\t\t\thadTruncatedLines = true;\n\t\t\t\t\t\t\t\treturn line.slice(0, MAX_LINE_LENGTH);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn line;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tlet outputText = formattedLines.join(\"\\n\");\n\n\t\t\t\t\t\t// Add notices\n\t\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\t\tif (hadTruncatedLines) {\n\t\t\t\t\t\t\tnotices.push(`Some lines were truncated to ${MAX_LINE_LENGTH} characters for display`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (endLine < lines.length) {\n\t\t\t\t\t\t\tconst remaining = lines.length - endLine;\n\t\t\t\t\t\t\tnotices.push(`${remaining} more lines not shown. Use offset=${endLine + 1} to continue reading`);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutputText += `\\n\\n... (${notices.join(\". \")})`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({ content, details: undefined });\n\t\t\t\t} catch (error: any) {\n\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\tif (signal) {\n\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import * as os from \"node:os\";\nimport type { AgentTool, ImageContent, TextContent } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { extname, resolve as resolvePath } from \"path\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn os.homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn os.homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\n/**\n * Map of file extensions to MIME types for common image formats\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".gif\": \"image/gif\",\n\t\".webp\": \"image/webp\",\n};\n\n/**\n * Check if a file is an image based on its extension\n */\nfunction isImageFile(filePath: string): string | null {\n\tconst ext = extname(filePath).toLowerCase();\n\treturn IMAGE_MIME_TYPES[ext] || null;\n}\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\ninterface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\nexport const readTool: AgentTool<typeof readSchema> = {\n\tname: \"read\",\n\tlabel: \"read\",\n\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files.`,\n\tparameters: readSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\tconst absolutePath = resolvePath(expandPath(path));\n\t\tconst mimeType = isImageFile(absolutePath);\n\n\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t(resolve, reject) => {\n\t\t\t\t// Check if already aborted\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet aborted = false;\n\n\t\t\t\t// Set up abort handler\n\t\t\t\tconst onAbort = () => {\n\t\t\t\t\taborted = true;\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\n\t\t\t\t// Perform the read operation\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Check if file exists\n\t\t\t\t\t\tawait access(absolutePath, constants.R_OK);\n\n\t\t\t\t\t\t// Check if aborted before reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read the file based on type\n\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\n\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t// Read as image (binary)\n\t\t\t\t\t\t\tconst buffer = await readFile(absolutePath);\n\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\n\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t{ type: \"text\", text: `Read image file [${mimeType}]` },\n\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Read as text\n\t\t\t\t\t\t\tconst textContent = await readFile(absolutePath, \"utf-8\");\n\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\n\t\t\t\t\t\t\t// Apply offset if specified (1-indexed to 0-indexed)\n\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1; // For display (1-indexed)\n\n\t\t\t\t\t\t\t// Check if offset is out of bounds\n\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// If limit is specified by user, use it; otherwise we'll let truncateHead decide\n\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Apply truncation (respects both line and byte limits)\n\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\n\t\t\t\t\t\t\tlet outputText: string;\n\n\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t// First line at offset exceeds 30KB - tell model to use bash\n\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t// Truncation occurred - build actionable notice\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\n\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t// User specified limit, there's more content, but no truncation\n\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + userLimitedLines - 1;\n\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\toutputText += `\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// No truncation, no user limit exceeded\n\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if aborted after reading\n\t\t\t\t\t\tif (aborted) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t} catch (error: any) {\n\t\t\t\t\t\t// Clean up abort handler\n\t\t\t\t\t\tif (signal) {\n\t\t\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!aborted) {\n\t\t\t\t\t\t\treject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t},\n\t\t);\n\t},\n};\n"]}
|