@mariozechner/pi-coding-agent 0.13.1 → 0.14.1

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +62 -0
  3. package/dist/compaction.d.ts.map +1 -1
  4. package/dist/compaction.js +5 -2
  5. package/dist/compaction.js.map +1 -1
  6. package/dist/export-html.d.ts.map +1 -1
  7. package/dist/export-html.js +52 -25
  8. package/dist/export-html.js.map +1 -1
  9. package/dist/main.d.ts.map +1 -1
  10. package/dist/main.js +120 -9
  11. package/dist/main.js.map +1 -1
  12. package/dist/messages.d.ts +43 -0
  13. package/dist/messages.d.ts.map +1 -0
  14. package/dist/messages.js +71 -0
  15. package/dist/messages.js.map +1 -0
  16. package/dist/model-config.d.ts.map +1 -1
  17. package/dist/model-config.js +9 -0
  18. package/dist/model-config.js.map +1 -1
  19. package/dist/settings-manager.d.ts +6 -3
  20. package/dist/settings-manager.d.ts.map +1 -1
  21. package/dist/settings-manager.js +7 -0
  22. package/dist/settings-manager.js.map +1 -1
  23. package/dist/shell.d.ts +16 -0
  24. package/dist/shell.d.ts.map +1 -0
  25. package/dist/shell.js +108 -0
  26. package/dist/shell.js.map +1 -0
  27. package/dist/theme/dark.json +4 -1
  28. package/dist/theme/light.json +4 -1
  29. package/dist/theme/theme-schema.json +28 -0
  30. package/dist/theme/theme.d.ts +3 -2
  31. package/dist/theme/theme.d.ts.map +1 -1
  32. package/dist/theme/theme.js +32 -3
  33. package/dist/theme/theme.js.map +1 -1
  34. package/dist/tools/bash.d.ts.map +1 -1
  35. package/dist/tools/bash.js +87 -153
  36. package/dist/tools/bash.js.map +1 -1
  37. package/dist/tools/find.d.ts.map +1 -1
  38. package/dist/tools/find.js +49 -28
  39. package/dist/tools/find.js.map +1 -1
  40. package/dist/tools/grep.d.ts.map +1 -1
  41. package/dist/tools/grep.js +37 -9
  42. package/dist/tools/grep.js.map +1 -1
  43. package/dist/tools/ls.d.ts.map +1 -1
  44. package/dist/tools/ls.js +28 -10
  45. package/dist/tools/ls.js.map +1 -1
  46. package/dist/tools/read.d.ts.map +1 -1
  47. package/dist/tools/read.js +52 -31
  48. package/dist/tools/read.js.map +1 -1
  49. package/dist/tools/truncate.d.ts +66 -0
  50. package/dist/tools/truncate.d.ts.map +1 -0
  51. package/dist/tools/truncate.js +195 -0
  52. package/dist/tools/truncate.js.map +1 -0
  53. package/dist/tui/bash-execution.d.ts +33 -0
  54. package/dist/tui/bash-execution.d.ts.map +1 -0
  55. package/dist/tui/bash-execution.js +134 -0
  56. package/dist/tui/bash-execution.js.map +1 -0
  57. package/dist/tui/tool-execution.d.ts.map +1 -1
  58. package/dist/tui/tool-execution.js +81 -5
  59. package/dist/tui/tool-execution.js.map +1 -1
  60. package/dist/tui/tui-renderer.d.ts +7 -1
  61. package/dist/tui/tui-renderer.d.ts.map +1 -1
  62. package/dist/tui/tui-renderer.js +225 -10
  63. package/dist/tui/tui-renderer.js.map +1 -1
  64. package/package.json +4 -4
  65. package/dist/fuzzy.test.d.ts +0 -2
  66. package/dist/fuzzy.test.d.ts.map +0 -1
  67. package/dist/fuzzy.test.js +0 -76
  68. package/dist/fuzzy.test.js.map +0 -1
@@ -1,110 +1,17 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createWriteStream } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
1
5
  import { Type } from "@sinclair/typebox";
2
- import { spawn, spawnSync } from "child_process";
3
- import { existsSync } from "fs";
4
- import { SettingsManager } from "../settings-manager.js";
5
- let cachedShellConfig = null;
6
+ import { spawn } from "child_process";
7
+ import { getShellConfig, killProcessTree } from "../shell.js";
8
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
6
9
  /**
7
- * Find bash executable on PATH (Windows)
10
+ * Generate a unique temp file path for bash output
8
11
  */
9
- function findBashOnPath() {
10
- try {
11
- const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 });
12
- if (result.status === 0 && result.stdout) {
13
- const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
14
- if (firstMatch && existsSync(firstMatch)) {
15
- return firstMatch;
16
- }
17
- }
18
- }
19
- catch {
20
- // Ignore errors
21
- }
22
- return null;
23
- }
24
- /**
25
- * Get shell configuration based on platform.
26
- * Resolution order:
27
- * 1. User-specified shellPath in settings.json
28
- * 2. On Windows: Git Bash in known locations
29
- * 3. Fallback: bash on PATH (Windows) or sh (Unix)
30
- */
31
- function getShellConfig() {
32
- if (cachedShellConfig) {
33
- return cachedShellConfig;
34
- }
35
- const settings = new SettingsManager();
36
- const customShellPath = settings.getShellPath();
37
- // 1. Check user-specified shell path
38
- if (customShellPath) {
39
- if (existsSync(customShellPath)) {
40
- cachedShellConfig = { shell: customShellPath, args: ["-c"] };
41
- return cachedShellConfig;
42
- }
43
- throw new Error(`Custom shell path not found: ${customShellPath}\n` + `Please update shellPath in ~/.pi/agent/settings.json`);
44
- }
45
- if (process.platform === "win32") {
46
- // 2. Try Git Bash in known locations
47
- const paths = [];
48
- const programFiles = process.env.ProgramFiles;
49
- if (programFiles) {
50
- paths.push(`${programFiles}\\Git\\bin\\bash.exe`);
51
- }
52
- const programFilesX86 = process.env["ProgramFiles(x86)"];
53
- if (programFilesX86) {
54
- paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
55
- }
56
- for (const path of paths) {
57
- if (existsSync(path)) {
58
- cachedShellConfig = { shell: path, args: ["-c"] };
59
- return cachedShellConfig;
60
- }
61
- }
62
- // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
63
- const bashOnPath = findBashOnPath();
64
- if (bashOnPath) {
65
- cachedShellConfig = { shell: bashOnPath, args: ["-c"] };
66
- return cachedShellConfig;
67
- }
68
- throw new Error(`No bash shell found. Options:\n` +
69
- ` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
70
- ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
71
- ` 3. Set shellPath in ~/.pi/agent/settings.json\n\n` +
72
- `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`);
73
- }
74
- cachedShellConfig = { shell: "sh", args: ["-c"] };
75
- return cachedShellConfig;
76
- }
77
- /**
78
- * Kill a process and all its children
79
- */
80
- function killProcessTree(pid) {
81
- if (process.platform === "win32") {
82
- // Use taskkill on Windows to kill process tree
83
- try {
84
- spawn("taskkill", ["/F", "/T", "/PID", String(pid)], {
85
- stdio: "ignore",
86
- detached: true,
87
- });
88
- }
89
- catch (e) {
90
- // Ignore errors if taskkill fails
91
- }
92
- }
93
- else {
94
- // Use SIGKILL on Unix/Linux/Mac
95
- try {
96
- process.kill(-pid, "SIGKILL");
97
- }
98
- catch (e) {
99
- // Fallback to killing just the child if process group kill fails
100
- try {
101
- process.kill(pid, "SIGKILL");
102
- }
103
- catch (e2) {
104
- // Process already dead
105
- }
106
- }
107
- }
12
+ function getTempFilePath() {
13
+ const id = randomBytes(8).toString("hex");
14
+ return join(tmpdir(), `pi-bash-${id}.log`);
108
15
  }
109
16
  const bashSchema = Type.Object({
110
17
  command: Type.String({ description: "Bash command to execute" }),
@@ -113,17 +20,24 @@ const bashSchema = Type.Object({
113
20
  export const bashTool = {
114
21
  name: "bash",
115
22
  label: "bash",
116
- description: "Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.",
23
+ description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
117
24
  parameters: bashSchema,
118
25
  execute: async (_toolCallId, { command, timeout }, signal) => {
119
- return new Promise((resolve, _reject) => {
26
+ return new Promise((resolve, reject) => {
120
27
  const { shell, args } = getShellConfig();
121
28
  const child = spawn(shell, [...args, command], {
122
29
  detached: true,
123
30
  stdio: ["ignore", "pipe", "pipe"],
124
31
  });
125
- let stdout = "";
126
- let stderr = "";
32
+ // We'll stream to a temp file if output gets large
33
+ let tempFilePath;
34
+ let tempFileStream;
35
+ let totalBytes = 0;
36
+ // Keep a rolling buffer of the last chunk for tail truncation
37
+ const chunks = [];
38
+ let chunksBytes = 0;
39
+ // Keep more than we need so we have enough for truncation
40
+ const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
127
41
  let timedOut = false;
128
42
  // Set timeout if provided
129
43
  let timeoutHandle;
@@ -133,25 +47,36 @@ export const bashTool = {
133
47
  onAbort();
134
48
  }, timeout * 1000);
135
49
  }
136
- // Collect stdout
137
- if (child.stdout) {
138
- child.stdout.on("data", (data) => {
139
- stdout += data.toString();
140
- // Limit buffer size
141
- if (stdout.length > 10 * 1024 * 1024) {
142
- stdout = stdout.slice(0, 10 * 1024 * 1024);
50
+ const handleData = (data) => {
51
+ totalBytes += data.length;
52
+ // Start writing to temp file once we exceed the threshold
53
+ if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
54
+ tempFilePath = getTempFilePath();
55
+ tempFileStream = createWriteStream(tempFilePath);
56
+ // Write all buffered chunks to the file
57
+ for (const chunk of chunks) {
58
+ tempFileStream.write(chunk);
143
59
  }
144
- });
60
+ }
61
+ // Write to temp file if we have one
62
+ if (tempFileStream) {
63
+ tempFileStream.write(data);
64
+ }
65
+ // Keep rolling buffer of recent data
66
+ chunks.push(data);
67
+ chunksBytes += data.length;
68
+ // Trim old chunks if buffer is too large
69
+ while (chunksBytes > maxChunksBytes && chunks.length > 1) {
70
+ const removed = chunks.shift();
71
+ chunksBytes -= removed.length;
72
+ }
73
+ };
74
+ // Collect stdout and stderr together
75
+ if (child.stdout) {
76
+ child.stdout.on("data", handleData);
145
77
  }
146
- // Collect stderr
147
78
  if (child.stderr) {
148
- child.stderr.on("data", (data) => {
149
- stderr += data.toString();
150
- // Limit buffer size
151
- if (stderr.length > 10 * 1024 * 1024) {
152
- stderr = stderr.slice(0, 10 * 1024 * 1024);
153
- }
154
- });
79
+ child.stderr.on("data", handleData);
155
80
  }
156
81
  // Handle process exit
157
82
  child.on("close", (code) => {
@@ -161,51 +86,60 @@ export const bashTool = {
161
86
  if (signal) {
162
87
  signal.removeEventListener("abort", onAbort);
163
88
  }
89
+ // Close temp file stream
90
+ if (tempFileStream) {
91
+ tempFileStream.end();
92
+ }
93
+ // Combine all buffered chunks
94
+ const fullBuffer = Buffer.concat(chunks);
95
+ const fullOutput = fullBuffer.toString("utf-8");
164
96
  if (signal?.aborted) {
165
- let output = "";
166
- if (stdout)
167
- output += stdout;
168
- if (stderr) {
169
- if (output)
170
- output += "\n";
171
- output += stderr;
172
- }
97
+ let output = fullOutput;
173
98
  if (output)
174
99
  output += "\n\n";
175
100
  output += "Command aborted";
176
- _reject(new Error(output));
101
+ reject(new Error(output));
177
102
  return;
178
103
  }
179
104
  if (timedOut) {
180
- let output = "";
181
- if (stdout)
182
- output += stdout;
183
- if (stderr) {
184
- if (output)
185
- output += "\n";
186
- output += stderr;
187
- }
105
+ let output = fullOutput;
188
106
  if (output)
189
107
  output += "\n\n";
190
108
  output += `Command timed out after ${timeout} seconds`;
191
- _reject(new Error(output));
109
+ reject(new Error(output));
192
110
  return;
193
111
  }
194
- let output = "";
195
- if (stdout)
196
- output += stdout;
197
- if (stderr) {
198
- if (output)
199
- output += "\n";
200
- output += stderr;
112
+ // Apply tail truncation
113
+ const truncation = truncateTail(fullOutput);
114
+ let outputText = truncation.content || "(no output)";
115
+ // Build details with truncation info
116
+ let details;
117
+ if (truncation.truncated) {
118
+ details = {
119
+ truncation,
120
+ fullOutputPath: tempFilePath,
121
+ };
122
+ // Build actionable notice
123
+ const startLine = truncation.totalLines - truncation.outputLines + 1;
124
+ const endLine = truncation.totalLines;
125
+ if (truncation.lastLinePartial) {
126
+ // Edge case: last line alone > 30KB
127
+ const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
128
+ outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
129
+ }
130
+ else if (truncation.truncatedBy === "lines") {
131
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
132
+ }
133
+ else {
134
+ outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
135
+ }
201
136
  }
202
137
  if (code !== 0 && code !== null) {
203
- if (output)
204
- output += "\n\n";
205
- _reject(new Error(`${output}Command exited with code ${code}`));
138
+ outputText += `\n\nCommand exited with code ${code}`;
139
+ reject(new Error(outputText));
206
140
  }
207
141
  else {
208
- resolve({ content: [{ type: "text", text: output || "(no output)" }], details: undefined });
142
+ resolve({ content: [{ type: "text", text: outputText }], details });
209
143
  }
210
144
  });
211
145
  // Handle abort signal - kill entire process tree
@@ -1 +1 @@
1
- {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { SettingsManager } from \"../settings-manager.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (Windows)\n */\nfunction findBashOnPath(): string | null {\n\ttry {\n\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations\n * 3. Fallback: bash on PATH (Windows) or sh (Unix)\n */\nfunction getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = new SettingsManager();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\n` + `Please update shellPath in ~/.pi/agent/settings.json`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ~/.pi/agent/settings.json\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\n/**\n * Kill a process and all its children\n */\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch (e) {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch (e) {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch (e2) {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription:\n\t\t\"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.\",\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, _reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\t// Collect stdout\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Collect stderr\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet output = \"\";\n\t\t\t\tif (stdout) output += stdout;\n\t\t\t\tif (stderr) {\n\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\toutput += stderr;\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\t_reject(new Error(`${output}Command exited with code ${code}`));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: output || \"(no output)\" }], details: undefined });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
1
+ {"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,eAAe,CAAC;AAEtH;;GAEG;AACH,SAAS,eAAe,GAAW;IAClC,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;AAAA,CAC3C;AAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,yBAAyB,EAAE,CAAC;IAChE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC,CAAC;CACzG,CAAC,CAAC;AAOH,MAAM,CAAC,MAAM,QAAQ,GAAiC;IACrD,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,WAAW,EAAE,mHAAmH,iBAAiB,aAAa,iBAAiB,GAAG,IAAI,0HAA0H;IAChT,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,KAAK,EACb,WAAmB,EACnB,EAAE,OAAO,EAAE,OAAO,EAAyC,EAC3D,MAAoB,EACnB,EAAE,CAAC;QACJ,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;YACvC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,cAAc,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE;gBAC9C,QAAQ,EAAE,IAAI;gBACd,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC;aACjC,CAAC,CAAC;YAEH,mDAAmD;YACnD,IAAI,YAAgC,CAAC;YACrC,IAAI,cAAgE,CAAC;YACrE,IAAI,UAAU,GAAG,CAAC,CAAC;YAEnB,8DAA8D;YAC9D,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,IAAI,WAAW,GAAG,CAAC,CAAC;YACpB,0DAA0D;YAC1D,MAAM,cAAc,GAAG,iBAAiB,GAAG,CAAC,CAAC;YAE7C,IAAI,QAAQ,GAAG,KAAK,CAAC;YAErB,0BAA0B;YAC1B,IAAI,aAAyC,CAAC;YAC9C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBAC1C,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;oBAChC,QAAQ,GAAG,IAAI,CAAC;oBAChB,OAAO,EAAE,CAAC;gBAAA,CACV,EAAE,OAAO,GAAG,IAAI,CAAC,CAAC;YACpB,CAAC;YAED,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC;gBACpC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC;gBAE1B,0DAA0D;gBAC1D,IAAI,UAAU,GAAG,iBAAiB,IAAI,CAAC,YAAY,EAAE,CAAC;oBACrD,YAAY,GAAG,eAAe,EAAE,CAAC;oBACjC,cAAc,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;oBACjD,wCAAwC;oBACxC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;wBAC5B,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBAC7B,CAAC;gBACF,CAAC;gBAED,oCAAoC;gBACpC,IAAI,cAAc,EAAE,CAAC;oBACpB,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC5B,CAAC;gBAED,qCAAqC;gBACrC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC;gBAE3B,yCAAyC;gBACzC,OAAO,WAAW,GAAG,cAAc,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1D,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,EAAG,CAAC;oBAChC,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC;gBAC/B,CAAC;YAAA,CACD,CAAC;YAEF,qCAAqC;YACrC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAClB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YACrC,CAAC;YACD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAClB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;YACrC,CAAC;YAED,sBAAsB;YACtB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC3B,IAAI,aAAa,EAAE,CAAC;oBACnB,YAAY,CAAC,aAAa,CAAC,CAAC;gBAC7B,CAAC;gBACD,IAAI,MAAM,EAAE,CAAC;oBACZ,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9C,CAAC;gBAED,yBAAyB;gBACzB,IAAI,cAAc,EAAE,CAAC;oBACpB,cAAc,CAAC,GAAG,EAAE,CAAC;gBACtB,CAAC;gBAED,8BAA8B;gBAC9B,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACzC,MAAM,UAAU,GAAG,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;gBAEhD,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACrB,IAAI,MAAM,GAAG,UAAU,CAAC;oBACxB,IAAI,MAAM;wBAAE,MAAM,IAAI,MAAM,CAAC;oBAC7B,MAAM,IAAI,iBAAiB,CAAC;oBAC5B,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;oBAC1B,OAAO;gBACR,CAAC;gBAED,IAAI,QAAQ,EAAE,CAAC;oBACd,IAAI,MAAM,GAAG,UAAU,CAAC;oBACxB,IAAI,MAAM;wBAAE,MAAM,IAAI,MAAM,CAAC;oBAC7B,MAAM,IAAI,2BAA2B,OAAO,UAAU,CAAC;oBACvD,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;oBAC1B,OAAO;gBACR,CAAC;gBAED,wBAAwB;gBACxB,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;gBAC5C,IAAI,UAAU,GAAG,UAAU,CAAC,OAAO,IAAI,aAAa,CAAC;gBAErD,qCAAqC;gBACrC,IAAI,OAAoC,CAAC;gBAEzC,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;oBAC1B,OAAO,GAAG;wBACT,UAAU;wBACV,cAAc,EAAE,YAAY;qBAC5B,CAAC;oBAEF,0BAA0B;oBAC1B,MAAM,SAAS,GAAG,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,WAAW,GAAG,CAAC,CAAC;oBACrE,MAAM,OAAO,GAAG,UAAU,CAAC,UAAU,CAAC;oBAEtC,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;wBAChC,oCAAoC;wBACpC,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;wBAChG,UAAU,IAAI,qBAAqB,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,YAAY,OAAO,aAAa,YAAY,mBAAmB,YAAY,GAAG,CAAC;oBACrJ,CAAC;yBAAM,IAAI,UAAU,CAAC,WAAW,KAAK,OAAO,EAAE,CAAC;wBAC/C,UAAU,IAAI,sBAAsB,SAAS,IAAI,OAAO,OAAO,UAAU,CAAC,UAAU,kBAAkB,YAAY,GAAG,CAAC;oBACvH,CAAC;yBAAM,CAAC;wBACP,UAAU,IAAI,sBAAsB,SAAS,IAAI,OAAO,OAAO,UAAU,CAAC,UAAU,KAAK,UAAU,CAAC,iBAAiB,CAAC,yBAAyB,YAAY,GAAG,CAAC;oBAChK,CAAC;gBACF,CAAC;gBAED,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;oBACjC,UAAU,IAAI,gCAAgC,IAAI,EAAE,CAAC;oBACrD,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;gBAC/B,CAAC;qBAAM,CAAC;oBACP,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;gBACrE,CAAC;YAAA,CACD,CAAC,CAAC;YAEH,iDAAiD;YACjD,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;gBACrB,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;oBACf,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC5B,CAAC;YAAA,CACD,CAAC;YAEF,IAAI,MAAM,EAAE,CAAC;gBACZ,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;oBACpB,OAAO,EAAE,CAAC;gBACX,CAAC;qBAAM,CAAC;oBACP,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3D,CAAC;YACF,CAAC;QAAA,CACD,CAAC,CAAC;IAAA,CACH;CACD,CAAC","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { getShellConfig, killProcessTree } from \"../shell.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `pi-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\t// We'll stream to a temp file if output gets large\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\t\t\tlet totalBytes = 0;\n\n\t\t\t// Keep a rolling buffer of the last chunk for tail truncation\n\t\t\tconst chunks: Buffer[] = [];\n\t\t\tlet chunksBytes = 0;\n\t\t\t// Keep more than we need so we have enough for truncation\n\t\t\tconst maxChunksBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\ttotalBytes += data.length;\n\n\t\t\t\t// Start writing to temp file once we exceed the threshold\n\t\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t\t// Write all buffered chunks to the file\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Write to temp file if we have one\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.write(data);\n\t\t\t\t}\n\n\t\t\t\t// Keep rolling buffer of recent data\n\t\t\t\tchunks.push(data);\n\t\t\t\tchunksBytes += data.length;\n\n\t\t\t\t// Trim old chunks if buffer is too large\n\t\t\t\twhile (chunksBytes > maxChunksBytes && chunks.length > 1) {\n\t\t\t\t\tconst removed = chunks.shift()!;\n\t\t\t\t\tchunksBytes -= removed.length;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Collect stdout and stderr together\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", handleData);\n\t\t\t}\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", handleData);\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\t// Close temp file stream\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.end();\n\t\t\t\t}\n\n\t\t\t\t// Combine all buffered chunks\n\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\tconst fullOutput = fullBuffer.toString(\"utf-8\");\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Apply tail truncation\n\t\t\t\tconst truncation = truncateTail(fullOutput);\n\t\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t\t// Build details with truncation info\n\t\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = {\n\t\t\t\t\t\ttruncation,\n\t\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t\t};\n\n\t\t\t\t\t// Build actionable notice\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\t// Edge case: last line alone > 30KB\n\t\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(fullOutput.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\toutputText += `\\n\\nCommand exited with code ${code}`;\n\t\t\t\t\treject(new Error(outputText));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: outputText }], details });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAsBrD,QAAA,MAAM,UAAU;;;;EAMd,CAAC;AAIH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAwIjD,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { globSync } from \"glob\";\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 findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst DEFAULT_LIMIT = 1000;\n\nexport const findTool: AgentTool<typeof findSchema> = {\n\tname: \"find\",\n\tlabel: \"find\",\n\tdescription:\n\t\t\"Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore.\",\n\tparameters: findSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },\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\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Ensure fd is available\n\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\treject(new Error(\"fd 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\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Build fd arguments\n\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\"--glob\", // Use glob pattern\n\t\t\t\t\t\t\"--color=never\", // No ANSI colors\n\t\t\t\t\t\t\"--hidden\", // Search hidden files (but still respect .gitignore)\n\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Include .gitignore files (root + nested) so fd respects them even outside git repos\n\t\t\t\t\tconst gitignoreFiles = new Set<string>();\n\t\t\t\t\tconst rootGitignore = path.join(searchPath, \".gitignore\");\n\t\t\t\t\tif (existsSync(rootGitignore)) {\n\t\t\t\t\t\tgitignoreFiles.add(rootGitignore);\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst nestedGitignores = globSync(\"**/.gitignore\", {\n\t\t\t\t\t\t\tcwd: searchPath,\n\t\t\t\t\t\t\tdot: true,\n\t\t\t\t\t\t\tabsolute: true,\n\t\t\t\t\t\t\tignore: [\"**/node_modules/**\", \"**/.git/**\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const file of nestedGitignores) {\n\t\t\t\t\t\t\tgitignoreFiles.add(file);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore glob errors\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const gitignorePath of gitignoreFiles) {\n\t\t\t\t\t\targs.push(\"--ignore-file\", gitignorePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pattern and path\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\t// Run fd\n\t\t\t\t\tconst result = spawnSync(fdPath, args, {\n\t\t\t\t\t\tencoding: \"utf-8\",\n\t\t\t\t\t\tmaxBuffer: 10 * 1024 * 1024, // 10MB\n\t\t\t\t\t});\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\treject(new Error(`Failed to run fd: ${result.error.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet output = result.stdout?.trim() || \"\";\n\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tconst errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;\n\t\t\t\t\t\t// fd returns non-zero for some errors but may still have partial output\n\t\t\t\t\t\tif (!output) {\n\t\t\t\t\t\t\treject(new Error(errorMsg));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!output) {\n\t\t\t\t\t\toutput = \"No files found matching pattern\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\t\tconst relativized: string[] = [];\n\n\t\t\t\t\t\tfor (const rawLine of lines) {\n\t\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\t\tif (!line) {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst hadTrailingSlash = line.endsWith(\"/\") || line.endsWith(\"\\\\\");\n\t\t\t\t\t\t\tlet relativePath = line;\n\t\t\t\t\t\t\tif (line.startsWith(searchPath)) {\n\t\t\t\t\t\t\t\trelativePath = line.slice(searchPath.length + 1); // +1 for the /\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\trelativePath = path.relative(searchPath, line);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (hadTrailingSlash && !relativePath.endsWith(\"/\")) {\n\t\t\t\t\t\t\t\trelativePath += \"/\";\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trelativized.push(relativePath);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\toutput = relativized.join(\"\\n\");\n\n\t\t\t\t\t\tconst count = relativized.length;\n\t\t\t\t\t\tif (count >= effectiveLimit) {\n\t\t\t\t\t\t\toutput += `\\n\\n(truncated, ${effectiveLimit} results shown)`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: output }], details: undefined });\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
1
+ {"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAuBrD,QAAA,MAAM,UAAU;;;;EAMd,CAAC;AASH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAoKjD,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { globSync } from \"glob\";\nimport { homedir } from \"os\";\nimport path from \"path\";\nimport { ensureTool } from \"../tools-manager.js\";\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 findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst DEFAULT_LIMIT = 1000;\n\ninterface FindToolDetails {\n\ttruncation?: TruncationResult;\n\tresultLimitReached?: number;\n}\n\nexport const findTool: AgentTool<typeof findSchema> = {\n\tname: \"find\",\n\tlabel: \"find\",\n\tdescription: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\tparameters: findSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },\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\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Ensure fd is available\n\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\treject(new Error(\"fd 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\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Build fd arguments\n\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\"--glob\", // Use glob pattern\n\t\t\t\t\t\t\"--color=never\", // No ANSI colors\n\t\t\t\t\t\t\"--hidden\", // Search hidden files (but still respect .gitignore)\n\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Include .gitignore files (root + nested) so fd respects them even outside git repos\n\t\t\t\t\tconst gitignoreFiles = new Set<string>();\n\t\t\t\t\tconst rootGitignore = path.join(searchPath, \".gitignore\");\n\t\t\t\t\tif (existsSync(rootGitignore)) {\n\t\t\t\t\t\tgitignoreFiles.add(rootGitignore);\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst nestedGitignores = globSync(\"**/.gitignore\", {\n\t\t\t\t\t\t\tcwd: searchPath,\n\t\t\t\t\t\t\tdot: true,\n\t\t\t\t\t\t\tabsolute: true,\n\t\t\t\t\t\t\tignore: [\"**/node_modules/**\", \"**/.git/**\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const file of nestedGitignores) {\n\t\t\t\t\t\t\tgitignoreFiles.add(file);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore glob errors\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const gitignorePath of gitignoreFiles) {\n\t\t\t\t\t\targs.push(\"--ignore-file\", gitignorePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pattern and path\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\t// Run fd\n\t\t\t\t\tconst result = spawnSync(fdPath, args, {\n\t\t\t\t\t\tencoding: \"utf-8\",\n\t\t\t\t\t\tmaxBuffer: 10 * 1024 * 1024, // 10MB\n\t\t\t\t\t});\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\treject(new Error(`Failed to run fd: ${result.error.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst output = result.stdout?.trim() || \"\";\n\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tconst errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;\n\t\t\t\t\t\t// fd returns non-zero for some errors but may still have partial output\n\t\t\t\t\t\tif (!output) {\n\t\t\t\t\t\t\treject(new Error(errorMsg));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!output) {\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"No files found matching pattern\" }],\n\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst relativized: string[] = [];\n\n\t\t\t\t\tfor (const rawLine of lines) {\n\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\tif (!line) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst hadTrailingSlash = line.endsWith(\"/\") || line.endsWith(\"\\\\\");\n\t\t\t\t\t\tlet relativePath = line;\n\t\t\t\t\t\tif (line.startsWith(searchPath)) {\n\t\t\t\t\t\t\trelativePath = line.slice(searchPath.length + 1); // +1 for the /\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = path.relative(searchPath, line);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (hadTrailingSlash && !relativePath.endsWith(\"/\")) {\n\t\t\t\t\t\t\trelativePath += \"/\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trelativized.push(relativePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if we hit the result limit\n\t\t\t\t\tconst resultLimitReached = relativized.length >= effectiveLimit;\n\n\t\t\t\t\t// Apply byte truncation (no line limit since we already have result limit)\n\t\t\t\t\tconst rawOutput = relativized.join(\"\\n\");\n\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\tlet resultOutput = truncation.content;\n\t\t\t\t\tconst details: FindToolDetails = {};\n\n\t\t\t\t\t// Build notices\n\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\tif (resultLimitReached) {\n\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdetails.resultLimitReached = effectiveLimit;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\tresultOutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultOutput }],\n\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
@@ -5,6 +5,7 @@ import { globSync } from "glob";
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, truncateHead } from "./truncate.js";
8
9
  /**
9
10
  * Expand ~ to home directory
10
11
  */
@@ -28,7 +29,7 @@ const DEFAULT_LIMIT = 1000;
28
29
  export const findTool = {
29
30
  name: "find",
30
31
  label: "find",
31
- description: "Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore.",
32
+ description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
32
33
  parameters: findSchema,
33
34
  execute: async (_toolCallId, { pattern, path: searchDir, limit }, signal) => {
34
35
  return new Promise((resolve, reject) => {
@@ -91,7 +92,7 @@ export const findTool = {
91
92
  reject(new Error(`Failed to run fd: ${result.error.message}`));
92
93
  return;
93
94
  }
94
- let output = result.stdout?.trim() || "";
95
+ const output = result.stdout?.trim() || "";
95
96
  if (result.status !== 0) {
96
97
  const errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;
97
98
  // fd returns non-zero for some errors but may still have partial output
@@ -101,36 +102,56 @@ export const findTool = {
101
102
  }
102
103
  }
103
104
  if (!output) {
104
- output = "No files found matching pattern";
105
+ resolve({
106
+ content: [{ type: "text", text: "No files found matching pattern" }],
107
+ details: undefined,
108
+ });
109
+ return;
105
110
  }
106
- else {
107
- const lines = output.split("\n");
108
- const relativized = [];
109
- for (const rawLine of lines) {
110
- const line = rawLine.replace(/\r$/, "").trim();
111
- if (!line) {
112
- continue;
113
- }
114
- const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
115
- let relativePath = line;
116
- if (line.startsWith(searchPath)) {
117
- relativePath = line.slice(searchPath.length + 1); // +1 for the /
118
- }
119
- else {
120
- relativePath = path.relative(searchPath, line);
121
- }
122
- if (hadTrailingSlash && !relativePath.endsWith("/")) {
123
- relativePath += "/";
124
- }
125
- relativized.push(relativePath);
111
+ const lines = output.split("\n");
112
+ const relativized = [];
113
+ for (const rawLine of lines) {
114
+ const line = rawLine.replace(/\r$/, "").trim();
115
+ if (!line) {
116
+ continue;
117
+ }
118
+ const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
119
+ let relativePath = line;
120
+ if (line.startsWith(searchPath)) {
121
+ relativePath = line.slice(searchPath.length + 1); // +1 for the /
122
+ }
123
+ else {
124
+ relativePath = path.relative(searchPath, line);
126
125
  }
127
- output = relativized.join("\n");
128
- const count = relativized.length;
129
- if (count >= effectiveLimit) {
130
- output += `\n\n(truncated, ${effectiveLimit} results shown)`;
126
+ if (hadTrailingSlash && !relativePath.endsWith("/")) {
127
+ relativePath += "/";
131
128
  }
129
+ relativized.push(relativePath);
132
130
  }
133
- resolve({ content: [{ type: "text", text: output }], details: undefined });
131
+ // Check if we hit the result limit
132
+ const resultLimitReached = relativized.length >= effectiveLimit;
133
+ // Apply byte truncation (no line limit since we already have result limit)
134
+ const rawOutput = relativized.join("\n");
135
+ const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
136
+ let resultOutput = truncation.content;
137
+ const details = {};
138
+ // Build notices
139
+ const notices = [];
140
+ if (resultLimitReached) {
141
+ notices.push(`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`);
142
+ details.resultLimitReached = effectiveLimit;
143
+ }
144
+ if (truncation.truncated) {
145
+ notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
146
+ details.truncation = truncation;
147
+ }
148
+ if (notices.length > 0) {
149
+ resultOutput += `\n\n[${notices.join(". ")}]`;
150
+ }
151
+ resolve({
152
+ content: [{ type: "text", text: resultOutput }],
153
+ details: Object.keys(details).length > 0 ? details : undefined,
154
+ });
134
155
  }
135
156
  catch (e) {
136
157
  signal?.removeEventListener("abort", onAbort);
@@ -1 +1 @@
1
- {"version":3,"file":"find.js","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { globSync } from \"glob\";\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 findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst DEFAULT_LIMIT = 1000;\n\nexport const findTool: AgentTool<typeof findSchema> = {\n\tname: \"find\",\n\tlabel: \"find\",\n\tdescription:\n\t\t\"Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore.\",\n\tparameters: findSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },\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\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Ensure fd is available\n\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\treject(new Error(\"fd 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\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Build fd arguments\n\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\"--glob\", // Use glob pattern\n\t\t\t\t\t\t\"--color=never\", // No ANSI colors\n\t\t\t\t\t\t\"--hidden\", // Search hidden files (but still respect .gitignore)\n\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Include .gitignore files (root + nested) so fd respects them even outside git repos\n\t\t\t\t\tconst gitignoreFiles = new Set<string>();\n\t\t\t\t\tconst rootGitignore = path.join(searchPath, \".gitignore\");\n\t\t\t\t\tif (existsSync(rootGitignore)) {\n\t\t\t\t\t\tgitignoreFiles.add(rootGitignore);\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst nestedGitignores = globSync(\"**/.gitignore\", {\n\t\t\t\t\t\t\tcwd: searchPath,\n\t\t\t\t\t\t\tdot: true,\n\t\t\t\t\t\t\tabsolute: true,\n\t\t\t\t\t\t\tignore: [\"**/node_modules/**\", \"**/.git/**\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const file of nestedGitignores) {\n\t\t\t\t\t\t\tgitignoreFiles.add(file);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore glob errors\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const gitignorePath of gitignoreFiles) {\n\t\t\t\t\t\targs.push(\"--ignore-file\", gitignorePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pattern and path\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\t// Run fd\n\t\t\t\t\tconst result = spawnSync(fdPath, args, {\n\t\t\t\t\t\tencoding: \"utf-8\",\n\t\t\t\t\t\tmaxBuffer: 10 * 1024 * 1024, // 10MB\n\t\t\t\t\t});\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\treject(new Error(`Failed to run fd: ${result.error.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tlet output = result.stdout?.trim() || \"\";\n\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tconst errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;\n\t\t\t\t\t\t// fd returns non-zero for some errors but may still have partial output\n\t\t\t\t\t\tif (!output) {\n\t\t\t\t\t\t\treject(new Error(errorMsg));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!output) {\n\t\t\t\t\t\toutput = \"No files found matching pattern\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\t\tconst relativized: string[] = [];\n\n\t\t\t\t\t\tfor (const rawLine of lines) {\n\t\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\t\tif (!line) {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst hadTrailingSlash = line.endsWith(\"/\") || line.endsWith(\"\\\\\");\n\t\t\t\t\t\t\tlet relativePath = line;\n\t\t\t\t\t\t\tif (line.startsWith(searchPath)) {\n\t\t\t\t\t\t\t\trelativePath = line.slice(searchPath.length + 1); // +1 for the /\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\trelativePath = path.relative(searchPath, line);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (hadTrailingSlash && !relativePath.endsWith(\"/\")) {\n\t\t\t\t\t\t\t\trelativePath += \"/\";\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trelativized.push(relativePath);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\toutput = relativized.join(\"\\n\");\n\n\t\t\t\t\t\tconst count = relativized.length;\n\t\t\t\t\t\tif (count >= effectiveLimit) {\n\t\t\t\t\t\t\toutput += `\\n\\n(truncated, ${effectiveLimit} results shown)`;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: output }], details: undefined });\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
1
+ {"version":3,"file":"find.js","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { globSync } from \"glob\";\nimport { homedir } from \"os\";\nimport path from \"path\";\nimport { ensureTool } from \"../tools-manager.js\";\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 findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst DEFAULT_LIMIT = 1000;\n\ninterface FindToolDetails {\n\ttruncation?: TruncationResult;\n\tresultLimitReached?: number;\n}\n\nexport const findTool: AgentTool<typeof findSchema> = {\n\tname: \"find\",\n\tlabel: \"find\",\n\tdescription: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\tparameters: findSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },\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\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Ensure fd is available\n\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\treject(new Error(\"fd 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\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Build fd arguments\n\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\"--glob\", // Use glob pattern\n\t\t\t\t\t\t\"--color=never\", // No ANSI colors\n\t\t\t\t\t\t\"--hidden\", // Search hidden files (but still respect .gitignore)\n\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Include .gitignore files (root + nested) so fd respects them even outside git repos\n\t\t\t\t\tconst gitignoreFiles = new Set<string>();\n\t\t\t\t\tconst rootGitignore = path.join(searchPath, \".gitignore\");\n\t\t\t\t\tif (existsSync(rootGitignore)) {\n\t\t\t\t\t\tgitignoreFiles.add(rootGitignore);\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst nestedGitignores = globSync(\"**/.gitignore\", {\n\t\t\t\t\t\t\tcwd: searchPath,\n\t\t\t\t\t\t\tdot: true,\n\t\t\t\t\t\t\tabsolute: true,\n\t\t\t\t\t\t\tignore: [\"**/node_modules/**\", \"**/.git/**\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const file of nestedGitignores) {\n\t\t\t\t\t\t\tgitignoreFiles.add(file);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore glob errors\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const gitignorePath of gitignoreFiles) {\n\t\t\t\t\t\targs.push(\"--ignore-file\", gitignorePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pattern and path\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\t// Run fd\n\t\t\t\t\tconst result = spawnSync(fdPath, args, {\n\t\t\t\t\t\tencoding: \"utf-8\",\n\t\t\t\t\t\tmaxBuffer: 10 * 1024 * 1024, // 10MB\n\t\t\t\t\t});\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\treject(new Error(`Failed to run fd: ${result.error.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst output = result.stdout?.trim() || \"\";\n\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tconst errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;\n\t\t\t\t\t\t// fd returns non-zero for some errors but may still have partial output\n\t\t\t\t\t\tif (!output) {\n\t\t\t\t\t\t\treject(new Error(errorMsg));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!output) {\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"No files found matching pattern\" }],\n\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst relativized: string[] = [];\n\n\t\t\t\t\tfor (const rawLine of lines) {\n\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\tif (!line) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst hadTrailingSlash = line.endsWith(\"/\") || line.endsWith(\"\\\\\");\n\t\t\t\t\t\tlet relativePath = line;\n\t\t\t\t\t\tif (line.startsWith(searchPath)) {\n\t\t\t\t\t\t\trelativePath = line.slice(searchPath.length + 1); // +1 for the /\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = path.relative(searchPath, line);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (hadTrailingSlash && !relativePath.endsWith(\"/\")) {\n\t\t\t\t\t\t\trelativePath += \"/\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trelativized.push(relativePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if we hit the result limit\n\t\t\t\t\tconst resultLimitReached = relativized.length >= effectiveLimit;\n\n\t\t\t\t\t// Apply byte truncation (no line limit since we already have result limit)\n\t\t\t\t\tconst rawOutput = relativized.join(\"\\n\");\n\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\tlet resultOutput = truncation.content;\n\t\t\t\t\tconst details: FindToolDetails = {};\n\n\t\t\t\t\t// Build notices\n\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\tif (resultLimitReached) {\n\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdetails.resultLimitReached = effectiveLimit;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\tresultOutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultOutput }],\n\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"grep.d.ts","sourceRoot":"","sources":["../../src/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAqBrD,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAIH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAoOjD,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\";\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.d.ts","sourceRoot":"","sources":["../../src/tools/grep.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AA6BrD,QAAA,MAAM,UAAU;;;;;;;;EAYd,CAAC;AAUH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CA2QjD,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"]}