@oh-my-pi/pi-coding-agent 8.4.2 → 8.4.5
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 +14 -0
- package/package.json +14 -6
- package/src/cursor.ts +1 -1
- package/src/modes/components/model-selector.ts +43 -14
- package/src/modes/components/tool-execution.ts +1 -3
- package/src/modes/interactive-mode.ts +3 -3
- package/src/prompts/system/plan-mode-active.md +4 -0
- package/src/prompts/tools/enter-plan-mode.md +6 -0
- package/src/prompts/tools/find.md +3 -2
- package/src/prompts/tools/grep.md +1 -1
- package/src/session/agent-session.ts +5 -1
- package/src/session/agent-storage.ts +54 -1
- package/src/task/executor.ts +6 -6
- package/src/task/index.ts +1 -3
- package/src/task/worker-protocol.ts +4 -4
- package/src/task/worker.ts +1 -1
- package/src/tools/bash.ts +1 -3
- package/src/tools/enter-plan-mode.ts +11 -6
- package/src/tools/find.ts +74 -150
- package/src/tools/grep.ts +215 -109
- package/src/tools/index.ts +5 -5
- package/src/tools/output-meta.ts +2 -2
- package/src/tools/plan-mode-guard.ts +1 -1
- package/src/tools/python.ts +1 -3
- package/src/tools/read.ts +30 -20
package/src/tools/find.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
2
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import {
|
|
6
|
+
import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import type { Static } from "@sinclair/typebox";
|
|
8
8
|
import { Type } from "@sinclair/typebox";
|
|
9
9
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -13,28 +13,24 @@ import findDescription from "../prompts/tools/find.md" with { type: "text" };
|
|
|
13
13
|
import { renderFileList, renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import { ensureTool } from "../utils/tools-manager";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
|
+
import { runRg } from "./grep";
|
|
16
17
|
import { applyListLimit } from "./list-limit";
|
|
17
18
|
import type { OutputMeta } from "./output-meta";
|
|
18
19
|
import { resolveToCwd } from "./path-utils";
|
|
19
20
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
20
|
-
import {
|
|
21
|
+
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
21
22
|
import { toolResult } from "./tool-result";
|
|
22
23
|
import { type TruncationResult, truncateHead } from "./truncate";
|
|
23
24
|
|
|
24
25
|
const findSchema = Type.Object({
|
|
25
26
|
pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', '**/*.json'" }),
|
|
26
27
|
path: Type.Optional(Type.String({ description: "Directory to search (default: cwd)" })),
|
|
28
|
+
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories (default: true)" })),
|
|
27
29
|
limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })),
|
|
28
|
-
hidden: Type.Optional(Type.Boolean({ description: "Include hidden files (default: true)" })),
|
|
29
|
-
type: Type.Optional(
|
|
30
|
-
StringEnum(["file", "dir", "all"], {
|
|
31
|
-
description: "Filter: file, dir, or all (default: all)",
|
|
32
|
-
}),
|
|
33
|
-
),
|
|
34
30
|
});
|
|
35
31
|
|
|
36
32
|
const DEFAULT_LIMIT = 1000;
|
|
37
|
-
const
|
|
33
|
+
const RG_TIMEOUT_MS = 5000;
|
|
38
34
|
|
|
39
35
|
export interface FindToolDetails {
|
|
40
36
|
truncation?: TruncationResult;
|
|
@@ -60,53 +56,10 @@ export interface FindOperations {
|
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
export interface FindToolOptions {
|
|
63
|
-
/** Custom operations for find. Default: local filesystem +
|
|
59
|
+
/** Custom operations for find. Default: local filesystem + rg */
|
|
64
60
|
operations?: FindOperations;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
export interface FdResult {
|
|
68
|
-
stdout: string;
|
|
69
|
-
stderr: string;
|
|
70
|
-
exitCode: number | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Run fd command and capture output.
|
|
75
|
-
*
|
|
76
|
-
* @throws ToolAbortError if signal is aborted
|
|
77
|
-
*/
|
|
78
|
-
export async function runFd(fdPath: string, args: string[], signal?: AbortSignal): Promise<FdResult> {
|
|
79
|
-
const child = ptree.cspawn([fdPath, ...args], { signal });
|
|
80
|
-
|
|
81
|
-
let stdout: string;
|
|
82
|
-
try {
|
|
83
|
-
stdout = await child.nothrow().text();
|
|
84
|
-
} catch (err) {
|
|
85
|
-
if (err instanceof ptree.Exception && err.aborted) {
|
|
86
|
-
throw new ToolAbortError();
|
|
87
|
-
}
|
|
88
|
-
throw err;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let exitError: unknown;
|
|
92
|
-
try {
|
|
93
|
-
await child.exited;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
exitError = err;
|
|
96
|
-
if (err instanceof ptree.Exception && err.aborted) {
|
|
97
|
-
throw new ToolAbortError();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const exitCode = child.exitCode ?? (exitError instanceof ptree.Exception ? exitError.exitCode : null);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
stdout,
|
|
105
|
-
stderr: child.peekStderr(),
|
|
106
|
-
exitCode,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
63
|
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
111
64
|
public readonly name = "find";
|
|
112
65
|
public readonly label = "Find";
|
|
@@ -129,7 +82,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
129
82
|
_onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
|
|
130
83
|
context?: AgentToolContext,
|
|
131
84
|
): Promise<AgentToolResult<FindToolDetails>> {
|
|
132
|
-
const { pattern, path: searchDir, limit, hidden
|
|
85
|
+
const { pattern, path: searchDir, limit, hidden } = params;
|
|
133
86
|
|
|
134
87
|
return untilAborted(signal, async () => {
|
|
135
88
|
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
|
@@ -142,9 +95,19 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
142
95
|
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
143
96
|
return relative.length === 0 ? "." : relative;
|
|
144
97
|
})();
|
|
145
|
-
const
|
|
146
|
-
|
|
98
|
+
const normalizedPattern = pattern.trim();
|
|
99
|
+
if (!normalizedPattern) {
|
|
100
|
+
throw new ToolError("Pattern must not be empty");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawLimit = limit ?? DEFAULT_LIMIT;
|
|
104
|
+
const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
|
|
105
|
+
if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
|
|
106
|
+
throw new ToolError("Limit must be a positive number");
|
|
107
|
+
}
|
|
147
108
|
const includeHidden = hidden ?? true;
|
|
109
|
+
const globPattern = normalizedPattern.replace(/\\/g, "/");
|
|
110
|
+
const globMatcher = new Bun.Glob(globPattern);
|
|
148
111
|
|
|
149
112
|
// If custom operations provided with glob, use that instead of fd
|
|
150
113
|
if (this.customOps?.glob) {
|
|
@@ -152,7 +115,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
152
115
|
throw new ToolError(`Path not found: ${searchPath}`);
|
|
153
116
|
}
|
|
154
117
|
|
|
155
|
-
const results = await this.customOps.glob(
|
|
118
|
+
const results = await this.customOps.glob(normalizedPattern, searchPath, {
|
|
156
119
|
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
157
120
|
limit: effectiveLimit,
|
|
158
121
|
});
|
|
@@ -195,99 +158,51 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
195
158
|
return resultBuilder.done();
|
|
196
159
|
}
|
|
197
160
|
|
|
198
|
-
|
|
199
|
-
const fdPath = await ensureTool("fd", {
|
|
200
|
-
silent: true,
|
|
201
|
-
notify: message => context?.ui?.notify(message, "info"),
|
|
202
|
-
});
|
|
203
|
-
if (!fdPath) {
|
|
204
|
-
throw new ToolError("fd is not available and could not be downloaded");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Build fd arguments
|
|
208
|
-
// When pattern contains path separators (e.g. "reports/**"), use --full-path
|
|
209
|
-
// so fd matches against the full path, not just the filename.
|
|
210
|
-
// Also prepend **/ to anchor the pattern at any depth in the search path.
|
|
211
|
-
// Note: "**/foo.rs" is a glob construct (filename at any depth), not a path.
|
|
212
|
-
// Only patterns with real path components like "foo/bar" or "foo/**/bar" need --full-path.
|
|
213
|
-
const patternWithoutLeadingStarStar = pattern.replace(/^\*\*\//, "");
|
|
214
|
-
const hasPathSeparator =
|
|
215
|
-
patternWithoutLeadingStarStar.includes("/") || patternWithoutLeadingStarStar.includes("\\");
|
|
216
|
-
const effectivePattern = hasPathSeparator && !pattern.startsWith("**/") ? `**/${pattern}` : pattern;
|
|
217
|
-
const args: string[] = [
|
|
218
|
-
"--glob", // Use glob pattern
|
|
219
|
-
...(hasPathSeparator ? ["--full-path"] : []),
|
|
220
|
-
"--color=never", // No ANSI colors
|
|
221
|
-
"--max-results",
|
|
222
|
-
String(effectiveLimit),
|
|
223
|
-
];
|
|
224
|
-
|
|
225
|
-
if (includeHidden) {
|
|
226
|
-
args.push("--hidden");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Add type filter
|
|
230
|
-
if (effectiveType === "file") {
|
|
231
|
-
args.push("--type", "f");
|
|
232
|
-
} else if (effectiveType === "dir") {
|
|
233
|
-
args.push("--type", "d");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Include .gitignore files (root + nested) so fd respects them even outside git repos
|
|
237
|
-
const gitignoreFiles = new Set<string>();
|
|
238
|
-
const rootGitignore = path.join(searchPath, ".gitignore");
|
|
239
|
-
if (await Bun.file(rootGitignore).exists()) {
|
|
240
|
-
gitignoreFiles.add(rootGitignore);
|
|
241
|
-
}
|
|
242
|
-
|
|
161
|
+
let searchStat: Awaited<ReturnType<typeof fs.stat>>;
|
|
243
162
|
try {
|
|
244
|
-
|
|
245
|
-
"--hidden",
|
|
246
|
-
"--no-ignore",
|
|
247
|
-
"--type",
|
|
248
|
-
"f",
|
|
249
|
-
"--glob",
|
|
250
|
-
".gitignore",
|
|
251
|
-
"--exclude",
|
|
252
|
-
".git",
|
|
253
|
-
"--exclude",
|
|
254
|
-
"node_modules",
|
|
255
|
-
"--absolute-path",
|
|
256
|
-
searchPath,
|
|
257
|
-
];
|
|
258
|
-
const timeoutSignal = AbortSignal.timeout(FD_TIMEOUT_MS);
|
|
259
|
-
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
260
|
-
const { stdout: gitignoreStdout } = await runFd(fdPath, gitignoreArgs, combinedSignal);
|
|
261
|
-
for (const rawLine of gitignoreStdout.split("\n")) {
|
|
262
|
-
const file = rawLine.trim();
|
|
263
|
-
if (!file) continue;
|
|
264
|
-
gitignoreFiles.add(file);
|
|
265
|
-
}
|
|
163
|
+
searchStat = await fs.stat(searchPath);
|
|
266
164
|
} catch (err) {
|
|
267
|
-
if (err
|
|
268
|
-
throw
|
|
165
|
+
if (isEnoent(err)) {
|
|
166
|
+
throw new ToolError(`Path not found: ${searchPath}`);
|
|
269
167
|
}
|
|
270
|
-
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
if (!searchStat.isDirectory()) {
|
|
171
|
+
throw new ToolError(`Path is not a directory: ${searchPath}`);
|
|
271
172
|
}
|
|
272
173
|
|
|
273
|
-
|
|
274
|
-
|
|
174
|
+
// Default: use rg
|
|
175
|
+
const rgPath = await ensureTool("rg", {
|
|
176
|
+
silent: true,
|
|
177
|
+
notify: message => context?.ui?.notify(message, "info"),
|
|
178
|
+
});
|
|
179
|
+
if (!rgPath) {
|
|
180
|
+
throw new ToolError("rg is not available and could not be downloaded");
|
|
275
181
|
}
|
|
276
182
|
|
|
277
|
-
|
|
278
|
-
|
|
183
|
+
const args = [
|
|
184
|
+
"--files",
|
|
185
|
+
...(includeHidden ? ["--hidden"] : []),
|
|
186
|
+
"--no-require-git",
|
|
187
|
+
"--color=never",
|
|
188
|
+
"--glob",
|
|
189
|
+
"!**/.git/**",
|
|
190
|
+
"--glob",
|
|
191
|
+
"!**/node_modules/**",
|
|
192
|
+
searchPath,
|
|
193
|
+
];
|
|
279
194
|
|
|
280
|
-
// Run
|
|
281
|
-
const mainTimeoutSignal = AbortSignal.timeout(
|
|
195
|
+
// Run rg with timeout
|
|
196
|
+
const mainTimeoutSignal = AbortSignal.timeout(RG_TIMEOUT_MS);
|
|
282
197
|
const mainCombinedSignal = signal ? AbortSignal.any([signal, mainTimeoutSignal]) : mainTimeoutSignal;
|
|
283
|
-
const { stdout, stderr, exitCode } = await
|
|
198
|
+
const { stdout, stderr, exitCode } = await runRg(rgPath, args, mainCombinedSignal);
|
|
284
199
|
const output = stdout.trim();
|
|
285
200
|
|
|
286
|
-
//
|
|
201
|
+
// rg exit codes: 0 = found files, 1 = no matches, other = error
|
|
287
202
|
// Treat exit code 1 with no output as "no files found"
|
|
288
203
|
if (!output) {
|
|
289
204
|
if (exitCode !== 0 && exitCode !== 1) {
|
|
290
|
-
throw new ToolError(stderr.trim() || `
|
|
205
|
+
throw new ToolError(stderr.trim() || `rg failed (exit ${exitCode})`);
|
|
291
206
|
}
|
|
292
207
|
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
293
208
|
return toolResult(details).text("No files found matching pattern").done();
|
|
@@ -311,21 +226,34 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
311
226
|
} else {
|
|
312
227
|
relativePath = path.relative(searchPath, line);
|
|
313
228
|
}
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
229
|
+
const matchPath = relativePath.replace(/\\/g, "/");
|
|
230
|
+
if (!globMatcher.match(matchPath)) {
|
|
231
|
+
continue;
|
|
317
232
|
}
|
|
318
233
|
|
|
234
|
+
let mtimeMs = 0;
|
|
235
|
+
let isDirectory = false;
|
|
319
236
|
// Get mtime for sorting (files that fail to stat get mtime 0)
|
|
320
237
|
try {
|
|
321
238
|
const fullPath = path.join(searchPath, relativePath);
|
|
322
|
-
const stat = await
|
|
323
|
-
|
|
324
|
-
|
|
239
|
+
const stat = await fs.stat(fullPath);
|
|
240
|
+
mtimeMs = stat.mtimeMs;
|
|
241
|
+
isDirectory = stat.isDirectory();
|
|
325
242
|
} catch {
|
|
326
|
-
|
|
327
|
-
|
|
243
|
+
mtimeMs = 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
|
|
247
|
+
relativePath += "/";
|
|
328
248
|
}
|
|
249
|
+
|
|
250
|
+
relativized.push(relativePath);
|
|
251
|
+
mtimes.push(mtimeMs);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (relativized.length === 0) {
|
|
255
|
+
const details: FindToolDetails = { scopePath, fileCount: 0, files: [], truncated: false };
|
|
256
|
+
return toolResult(details).text("No files found matching pattern").done();
|
|
329
257
|
}
|
|
330
258
|
|
|
331
259
|
// Sort by mtime (most recent first)
|
|
@@ -373,8 +301,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
373
301
|
interface FindRenderArgs {
|
|
374
302
|
pattern: string;
|
|
375
303
|
path?: string;
|
|
376
|
-
type?: string;
|
|
377
|
-
hidden?: boolean;
|
|
378
304
|
sortByMtime?: boolean;
|
|
379
305
|
limit?: number;
|
|
380
306
|
}
|
|
@@ -386,8 +312,6 @@ export const findToolRenderer = {
|
|
|
386
312
|
renderCall(args: FindRenderArgs, uiTheme: Theme): Component {
|
|
387
313
|
const meta: string[] = [];
|
|
388
314
|
if (args.path) meta.push(`in ${args.path}`);
|
|
389
|
-
if (args.type && args.type !== "all") meta.push(`type:${args.type}`);
|
|
390
|
-
if (args.hidden) meta.push("hidden");
|
|
391
315
|
if (args.sortByMtime) meta.push("sort:mtime");
|
|
392
316
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
393
317
|
|