@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/grep.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import nodePath from "node:path";
|
|
1
|
+
import * as nodePath from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -23,31 +23,32 @@ import { toolResult } from "./tool-result";
|
|
|
23
23
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine } from "./truncate";
|
|
24
24
|
|
|
25
25
|
const grepSchema = Type.Object({
|
|
26
|
-
pattern: Type.String({ description: "
|
|
27
|
-
path: Type.Optional(Type.String({ description: "
|
|
28
|
-
glob: Type.Optional(Type.String({ description: "
|
|
29
|
-
type: Type.Optional(Type.String({ description: "
|
|
30
|
-
ignore_case: Type.Optional(Type.Boolean({ description: "Force case-insensitive (default: smart-case)" })),
|
|
31
|
-
case_sensitive: Type.Optional(Type.Boolean({ description: "Force case-sensitive (default: smart-case)" })),
|
|
32
|
-
literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal, not regex (default: false)" })),
|
|
33
|
-
multiline: Type.Optional(Type.Boolean({ description: "Match across line boundaries (default: false)" })),
|
|
34
|
-
context: Type.Optional(Type.Number({ description: "Lines of context before/after match (default: 0)" })),
|
|
35
|
-
limit: Type.Optional(Type.Number({ description: "Max matches to return (default: 100)" })),
|
|
26
|
+
pattern: Type.String({ description: "Regex pattern to search for" }),
|
|
27
|
+
path: Type.Optional(Type.String({ description: "File or directory to search (default: cwd)" })),
|
|
28
|
+
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern (e.g., '*.js')" })),
|
|
29
|
+
type: Type.Optional(Type.String({ description: "Filter by file type (e.g., js, py, rust)" })),
|
|
36
30
|
output_mode: Type.Optional(
|
|
37
|
-
StringEnum(["
|
|
38
|
-
description: "Output format (default:
|
|
31
|
+
StringEnum(["files_with_matches", "content", "count"], {
|
|
32
|
+
description: "Output format (default: files_with_matches)",
|
|
39
33
|
}),
|
|
40
34
|
),
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
i: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
36
|
+
n: Type.Optional(Type.Boolean({ description: "Show line numbers (default: true)" })),
|
|
37
|
+
a: Type.Optional(Type.Number({ description: "Lines to show after each match (default: 0)" })),
|
|
38
|
+
b: Type.Optional(Type.Number({ description: "Lines to show before each match (default: 0)" })),
|
|
39
|
+
c: Type.Optional(Type.Number({ description: "Lines of context (before and after) (default: 0)" })),
|
|
40
|
+
context: Type.Optional(Type.Number({ description: "Lines of context (alias for c)" })),
|
|
41
|
+
multiline: Type.Optional(Type.Boolean({ description: "Enable multiline matching (default: false)" })),
|
|
42
|
+
limit: Type.Optional(Type.Number({ description: "Limit output to first N matches (default: 100 in content mode)" })),
|
|
43
|
+
offset: Type.Optional(Type.Number({ description: "Skip first N entries before applying limit (default: 0)" })),
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
+
const DEFAULT_MATCH_LIMIT = 100;
|
|
46
47
|
|
|
47
48
|
export interface GrepToolDetails {
|
|
48
49
|
truncation?: TruncationResult;
|
|
49
50
|
matchLimitReached?: number;
|
|
50
|
-
|
|
51
|
+
resultLimitReached?: number;
|
|
51
52
|
linesTruncated?: boolean;
|
|
52
53
|
meta?: OutputMeta;
|
|
53
54
|
// Fields for TUI rendering
|
|
@@ -61,6 +62,49 @@ export interface GrepToolDetails {
|
|
|
61
62
|
error?: string;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
export interface RgResult {
|
|
66
|
+
stdout: string;
|
|
67
|
+
stderr: string;
|
|
68
|
+
exitCode: number | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run rg command and capture output.
|
|
73
|
+
*
|
|
74
|
+
* @throws ToolAbortError if signal is aborted
|
|
75
|
+
*/
|
|
76
|
+
export async function runRg(rgPath: string, args: string[], signal?: AbortSignal): Promise<RgResult> {
|
|
77
|
+
const child = ptree.cspawn([rgPath, ...args], { signal });
|
|
78
|
+
|
|
79
|
+
let stdout: string;
|
|
80
|
+
try {
|
|
81
|
+
stdout = await child.nothrow().text();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof ptree.Exception && err.aborted) {
|
|
84
|
+
throw new ToolAbortError();
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let exitError: unknown;
|
|
90
|
+
try {
|
|
91
|
+
await child.exited;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
exitError = err;
|
|
94
|
+
if (err instanceof ptree.Exception && err.aborted) {
|
|
95
|
+
throw new ToolAbortError();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const exitCode = child.exitCode ?? (exitError instanceof ptree.Exception ? exitError.exitCode : null);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
stdout,
|
|
103
|
+
stderr: child.peekStderr(),
|
|
104
|
+
exitCode,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
/**
|
|
65
109
|
* Pluggable operations for the grep tool.
|
|
66
110
|
* Override these to delegate search to remote systems (e.g., SSH).
|
|
@@ -87,14 +131,15 @@ interface GrepParams {
|
|
|
87
131
|
path?: string;
|
|
88
132
|
glob?: string;
|
|
89
133
|
type?: string;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
output_mode?: "content" | "files_with_matches" | "count";
|
|
135
|
+
i?: boolean;
|
|
136
|
+
n?: boolean;
|
|
137
|
+
a?: number;
|
|
138
|
+
b?: number;
|
|
139
|
+
c?: number;
|
|
94
140
|
context?: number;
|
|
141
|
+
multiline?: boolean;
|
|
95
142
|
limit?: number;
|
|
96
|
-
output_mode?: "content" | "files_with_matches" | "count";
|
|
97
|
-
head_limit?: number;
|
|
98
143
|
offset?: number;
|
|
99
144
|
}
|
|
100
145
|
|
|
@@ -149,20 +194,65 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
149
194
|
path: searchDir,
|
|
150
195
|
glob,
|
|
151
196
|
type,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
197
|
+
output_mode,
|
|
198
|
+
i,
|
|
199
|
+
n,
|
|
200
|
+
a,
|
|
201
|
+
b,
|
|
202
|
+
c,
|
|
156
203
|
context,
|
|
204
|
+
multiline,
|
|
157
205
|
limit,
|
|
158
|
-
output_mode,
|
|
159
|
-
head_limit,
|
|
160
206
|
offset,
|
|
161
207
|
} = params;
|
|
162
208
|
|
|
163
209
|
return untilAborted(signal, async () => {
|
|
164
|
-
|
|
165
|
-
|
|
210
|
+
const normalizedPattern = pattern.trim();
|
|
211
|
+
if (!normalizedPattern) {
|
|
212
|
+
throw new ToolError("Pattern must not be empty");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const normalizedOffset = offset === undefined ? 0 : Number.isFinite(offset) ? Math.floor(offset) : Number.NaN;
|
|
216
|
+
if (normalizedOffset < 0 || !Number.isFinite(normalizedOffset)) {
|
|
217
|
+
throw new ToolError("Offset must be a non-negative number");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rawLimit = limit === undefined ? undefined : Number.isFinite(limit) ? Math.floor(limit) : Number.NaN;
|
|
221
|
+
if (rawLimit !== undefined && (!Number.isFinite(rawLimit) || rawLimit < 0)) {
|
|
222
|
+
throw new ToolError("Limit must be a non-negative number");
|
|
223
|
+
}
|
|
224
|
+
const normalizedLimit = rawLimit !== undefined && rawLimit > 0 ? rawLimit : undefined;
|
|
225
|
+
|
|
226
|
+
const normalizeContext = (value: number | undefined, label: string): number => {
|
|
227
|
+
if (value === undefined) return 0;
|
|
228
|
+
const normalized = Number.isFinite(value) ? Math.floor(value) : Number.NaN;
|
|
229
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
230
|
+
throw new ToolError(`${label} must be a non-negative number`);
|
|
231
|
+
}
|
|
232
|
+
return normalized;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const normalizedAfter = normalizeContext(a, "After context");
|
|
236
|
+
const normalizedBefore = normalizeContext(b, "Before context");
|
|
237
|
+
const hasContextParam = context !== undefined;
|
|
238
|
+
const hasCParam = c !== undefined;
|
|
239
|
+
if (hasContextParam && hasCParam) {
|
|
240
|
+
throw new ToolError("Cannot combine context with c");
|
|
241
|
+
}
|
|
242
|
+
const normalizedContext = normalizeContext(hasContextParam ? context : c, "Context");
|
|
243
|
+
if (normalizedContext > 0 && (normalizedAfter > 0 || normalizedBefore > 0)) {
|
|
244
|
+
throw new ToolError("Cannot combine context with a or b");
|
|
245
|
+
}
|
|
246
|
+
const contextAfterValue = normalizedContext > 0 ? normalizedContext : normalizedAfter;
|
|
247
|
+
const contextBeforeValue = normalizedContext > 0 ? normalizedContext : normalizedBefore;
|
|
248
|
+
const showLineNumbers = n ?? true;
|
|
249
|
+
const ignoreCase = i ?? false;
|
|
250
|
+
const normalizedGlob = glob?.trim() ?? "";
|
|
251
|
+
const normalizedType = type?.trim() ?? "";
|
|
252
|
+
const hasContentHints =
|
|
253
|
+
limit !== undefined || context !== undefined || c !== undefined || a !== undefined || b !== undefined;
|
|
254
|
+
|
|
255
|
+
// Validate regex patterns early to surface parse errors before running rg
|
|
166
256
|
const rgPath = await ensureTool("rg", {
|
|
167
257
|
silent: true,
|
|
168
258
|
notify: message => toolContext?.ui?.notify(message, "info"),
|
|
@@ -172,12 +262,9 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
172
262
|
throw new ToolError("rg is not available and could not be downloaded");
|
|
173
263
|
}
|
|
174
264
|
|
|
175
|
-
|
|
176
|
-
if (!
|
|
177
|
-
|
|
178
|
-
if (!validation.valid) {
|
|
179
|
-
useLiteral = true;
|
|
180
|
-
}
|
|
265
|
+
const validation = await this.validateRegexPattern(normalizedPattern, rgPath);
|
|
266
|
+
if (!validation.valid) {
|
|
267
|
+
throw new ToolError(validation.error ?? "Invalid regex pattern");
|
|
181
268
|
}
|
|
182
269
|
|
|
183
270
|
// rgPath resolved earlier
|
|
@@ -193,10 +280,11 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
193
280
|
} catch {
|
|
194
281
|
throw new ToolError(`Path not found: ${searchPath}`);
|
|
195
282
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
283
|
+
const effectiveOutputMode =
|
|
284
|
+
output_mode ?? (!isDirectory || hasContentHints ? "content" : "files_with_matches");
|
|
285
|
+
const effectiveOffset = normalizedOffset > 0 ? normalizedOffset : 0;
|
|
286
|
+
const effectiveLimit =
|
|
287
|
+
effectiveOutputMode === "content" ? (normalizedLimit ?? DEFAULT_MATCH_LIMIT) : normalizedLimit;
|
|
200
288
|
|
|
201
289
|
const formatPath = (filePath: string): string => {
|
|
202
290
|
if (isDirectory) {
|
|
@@ -229,38 +317,46 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
229
317
|
|
|
230
318
|
// Base arguments depend on output mode
|
|
231
319
|
if (effectiveOutputMode === "files_with_matches") {
|
|
232
|
-
args.push("--files-with-matches", "--color=never"
|
|
320
|
+
args.push("--files-with-matches", "--color=never");
|
|
233
321
|
} else if (effectiveOutputMode === "count") {
|
|
234
|
-
args.push("--count", "--color=never"
|
|
322
|
+
args.push("--count", "--color=never");
|
|
235
323
|
} else {
|
|
236
|
-
args.push("--json", "--
|
|
324
|
+
args.push("--json", "--color=never");
|
|
325
|
+
if (showLineNumbers) {
|
|
326
|
+
args.push("--line-number");
|
|
327
|
+
}
|
|
237
328
|
}
|
|
238
329
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
330
|
+
args.push("--hidden");
|
|
331
|
+
|
|
332
|
+
if (ignoreCase) {
|
|
242
333
|
args.push("--ignore-case");
|
|
243
334
|
} else {
|
|
244
|
-
args.push("--
|
|
335
|
+
args.push("--case-sensitive");
|
|
245
336
|
}
|
|
246
337
|
|
|
247
338
|
if (multiline) {
|
|
248
339
|
args.push("--multiline");
|
|
249
340
|
}
|
|
250
341
|
|
|
251
|
-
if (
|
|
252
|
-
args.push("--
|
|
342
|
+
if (normalizedGlob) {
|
|
343
|
+
args.push("--glob", normalizedGlob);
|
|
253
344
|
}
|
|
254
345
|
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
args.push("--glob", "!**/.git/**");
|
|
347
|
+
args.push("--glob", "!**/node_modules/**");
|
|
348
|
+
|
|
349
|
+
if (normalizedType) {
|
|
350
|
+
args.push("--type", normalizedType);
|
|
257
351
|
}
|
|
258
352
|
|
|
259
|
-
if (
|
|
260
|
-
|
|
353
|
+
if (effectiveOutputMode === "content") {
|
|
354
|
+
if (normalizedContext > 0) {
|
|
355
|
+
args.push("-C", String(normalizedContext));
|
|
356
|
+
}
|
|
261
357
|
}
|
|
262
358
|
|
|
263
|
-
args.push("--",
|
|
359
|
+
args.push("--", normalizedPattern, searchPath);
|
|
264
360
|
|
|
265
361
|
const child = ptree.cspawn([rgPath, ...args], { signal });
|
|
266
362
|
|
|
@@ -320,8 +416,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
320
416
|
|
|
321
417
|
const offsetLines = effectiveOffset > 0 ? lines.slice(effectiveOffset) : lines;
|
|
322
418
|
const listLimit = applyListLimit(offsetLines, {
|
|
323
|
-
limit:
|
|
324
|
-
headLimit: head_limit,
|
|
419
|
+
limit: normalizedLimit,
|
|
325
420
|
limitType: "result",
|
|
326
421
|
});
|
|
327
422
|
const processedLines = listLimit.items;
|
|
@@ -348,13 +443,13 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
348
443
|
};
|
|
349
444
|
|
|
350
445
|
if (effectiveOutputMode === "files_with_matches") {
|
|
351
|
-
for (const line of
|
|
446
|
+
for (const line of processedLines) {
|
|
352
447
|
recordSimpleFile(line);
|
|
353
448
|
}
|
|
354
449
|
fileCount = simpleFiles.size;
|
|
355
450
|
simpleMatchCount = fileCount;
|
|
356
451
|
} else {
|
|
357
|
-
for (const line of
|
|
452
|
+
for (const line of processedLines) {
|
|
358
453
|
const separatorIndex = line.lastIndexOf(":");
|
|
359
454
|
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
360
455
|
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
@@ -368,7 +463,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
368
463
|
fileCount = simpleFiles.size;
|
|
369
464
|
}
|
|
370
465
|
|
|
371
|
-
const truncatedByLimit = Boolean(limitMeta.resultLimit
|
|
466
|
+
const truncatedByLimit = Boolean(limitMeta.resultLimit);
|
|
372
467
|
|
|
373
468
|
// For count mode, format as "path:count"
|
|
374
469
|
if (effectiveOutputMode === "count") {
|
|
@@ -390,13 +485,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
390
485
|
})),
|
|
391
486
|
mode: effectiveOutputMode,
|
|
392
487
|
truncated: truncatedByLimit,
|
|
393
|
-
|
|
488
|
+
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
394
489
|
};
|
|
395
490
|
return toolResult(details)
|
|
396
491
|
.text(output)
|
|
397
492
|
.limits({
|
|
398
493
|
resultLimit: limitMeta.resultLimit?.reached,
|
|
399
|
-
headLimit: limitMeta.headLimit?.reached,
|
|
400
494
|
})
|
|
401
495
|
.done();
|
|
402
496
|
}
|
|
@@ -411,13 +505,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
411
505
|
files: simpleFileList,
|
|
412
506
|
mode: effectiveOutputMode,
|
|
413
507
|
truncated: truncatedByLimit,
|
|
414
|
-
|
|
508
|
+
resultLimitReached: limitMeta.resultLimit?.reached,
|
|
415
509
|
};
|
|
416
510
|
return toolResult(details)
|
|
417
511
|
.text(output)
|
|
418
512
|
.limits({
|
|
419
513
|
resultLimit: limitMeta.resultLimit?.reached,
|
|
420
|
-
headLimit: limitMeta.headLimit?.reached,
|
|
421
514
|
})
|
|
422
515
|
.done();
|
|
423
516
|
}
|
|
@@ -427,12 +520,14 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
427
520
|
const relativePath = formatPath(filePath);
|
|
428
521
|
const lines = await getFileLines(filePath);
|
|
429
522
|
if (!lines.length) {
|
|
430
|
-
return
|
|
523
|
+
return showLineNumbers
|
|
524
|
+
? [`${relativePath}:${lineNumber}: (unable to read file)`]
|
|
525
|
+
: [`${relativePath}: (unable to read file)`];
|
|
431
526
|
}
|
|
432
527
|
|
|
433
528
|
const block: string[] = [];
|
|
434
|
-
const start =
|
|
435
|
-
const end =
|
|
529
|
+
const start = contextBeforeValue > 0 ? Math.max(1, lineNumber - contextBeforeValue) : lineNumber;
|
|
530
|
+
const end = contextAfterValue > 0 ? Math.min(lines.length, lineNumber + contextAfterValue) : lineNumber;
|
|
436
531
|
|
|
437
532
|
for (let current = start; current <= end; current++) {
|
|
438
533
|
const lineText = lines[current - 1] ?? "";
|
|
@@ -445,17 +540,26 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
445
540
|
}
|
|
446
541
|
|
|
447
542
|
if (isMatchLine) {
|
|
448
|
-
block.push(
|
|
543
|
+
block.push(
|
|
544
|
+
showLineNumbers
|
|
545
|
+
? `${relativePath}:${current}: ${truncatedText}`
|
|
546
|
+
: `${relativePath}: ${truncatedText}`,
|
|
547
|
+
);
|
|
449
548
|
} else {
|
|
450
|
-
block.push(
|
|
549
|
+
block.push(
|
|
550
|
+
showLineNumbers
|
|
551
|
+
? `${relativePath}-${current}- ${truncatedText}`
|
|
552
|
+
: `${relativePath}- ${truncatedText}`,
|
|
553
|
+
);
|
|
451
554
|
}
|
|
452
555
|
}
|
|
453
556
|
|
|
454
557
|
return block;
|
|
455
558
|
};
|
|
456
559
|
|
|
560
|
+
const maxMatches = effectiveLimit !== undefined ? effectiveLimit + effectiveOffset : undefined;
|
|
457
561
|
const processLine = async (line: string): Promise<void> => {
|
|
458
|
-
if (!line.trim()
|
|
562
|
+
if (!line.trim()) {
|
|
459
563
|
return;
|
|
460
564
|
}
|
|
461
565
|
|
|
@@ -467,22 +571,27 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
467
571
|
}
|
|
468
572
|
|
|
469
573
|
if (event.type === "match") {
|
|
470
|
-
matchCount
|
|
574
|
+
const nextIndex = matchCount + 1;
|
|
575
|
+
if (maxMatches !== undefined && nextIndex > maxMatches) {
|
|
576
|
+
matchLimitReached = true;
|
|
577
|
+
killedDueToLimit = true;
|
|
578
|
+
child.kill("SIGKILL");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
matchCount = nextIndex;
|
|
471
583
|
const filePath = event.data?.path?.text;
|
|
472
584
|
const lineNumber = event.data?.line_number;
|
|
473
585
|
|
|
474
586
|
if (filePath && typeof lineNumber === "number") {
|
|
587
|
+
if (matchCount <= effectiveOffset) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
475
590
|
recordFile(filePath);
|
|
476
591
|
recordFileMatch(filePath);
|
|
477
592
|
const block = await formatBlock(filePath, lineNumber);
|
|
478
593
|
outputLines.push(...block);
|
|
479
594
|
}
|
|
480
|
-
|
|
481
|
-
if (matchCount >= effectiveLimit) {
|
|
482
|
-
matchLimitReached = true;
|
|
483
|
-
killedDueToLimit = true;
|
|
484
|
-
child.kill("SIGKILL");
|
|
485
|
-
}
|
|
486
595
|
}
|
|
487
596
|
};
|
|
488
597
|
|
|
@@ -531,21 +640,20 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
531
640
|
return toolResult(details).text("No matches found").done();
|
|
532
641
|
}
|
|
533
642
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const limitMeta = listLimit.meta;
|
|
643
|
+
const limitMeta =
|
|
644
|
+
matchLimitReached && effectiveLimit !== undefined
|
|
645
|
+
? { matchLimit: { reached: effectiveLimit, suggestion: effectiveLimit * 2 } }
|
|
646
|
+
: {};
|
|
539
647
|
|
|
540
648
|
// Apply byte truncation (no line limit since we already have match limit)
|
|
541
|
-
const rawOutput =
|
|
649
|
+
const rawOutput = outputLines.join("\n");
|
|
542
650
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
543
651
|
|
|
544
652
|
const output = truncation.content;
|
|
545
|
-
const truncated = Boolean(matchLimitReached || truncation.truncated || limitMeta.
|
|
653
|
+
const truncated = Boolean(matchLimitReached || truncation.truncated || limitMeta.matchLimit || linesTruncated);
|
|
546
654
|
const details: GrepToolDetails = {
|
|
547
655
|
scopePath,
|
|
548
|
-
matchCount,
|
|
656
|
+
matchCount: effectiveOffset > 0 ? Math.max(0, matchCount - effectiveOffset) : matchCount,
|
|
549
657
|
fileCount: files.size,
|
|
550
658
|
files: fileList,
|
|
551
659
|
fileMatches: fileList.map(path => ({
|
|
@@ -554,19 +662,20 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
554
662
|
})),
|
|
555
663
|
mode: effectiveOutputMode,
|
|
556
664
|
truncated,
|
|
557
|
-
|
|
665
|
+
matchLimitReached: limitMeta.matchLimit?.reached,
|
|
558
666
|
};
|
|
559
667
|
|
|
560
668
|
// Keep TUI compatibility fields
|
|
561
|
-
if (matchLimitReached
|
|
669
|
+
if (matchLimitReached && effectiveLimit !== undefined) {
|
|
670
|
+
details.matchLimitReached = effectiveLimit;
|
|
671
|
+
}
|
|
562
672
|
if (truncation.truncated) details.truncation = truncation;
|
|
563
673
|
if (linesTruncated) details.linesTruncated = true;
|
|
564
674
|
|
|
565
675
|
const resultBuilder = toolResult(details)
|
|
566
676
|
.text(output)
|
|
567
677
|
.limits({
|
|
568
|
-
matchLimit:
|
|
569
|
-
headLimit: limitMeta.headLimit?.reached,
|
|
678
|
+
matchLimit: limitMeta.matchLimit?.reached,
|
|
570
679
|
columnMax: linesTruncated ? DEFAULT_MAX_COLUMN : undefined,
|
|
571
680
|
});
|
|
572
681
|
if (truncation.truncated) {
|
|
@@ -587,13 +696,16 @@ interface GrepRenderArgs {
|
|
|
587
696
|
path?: string;
|
|
588
697
|
glob?: string;
|
|
589
698
|
type?: string;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
699
|
+
i?: boolean;
|
|
700
|
+
n?: boolean;
|
|
701
|
+
a?: number;
|
|
702
|
+
b?: number;
|
|
703
|
+
c?: number;
|
|
594
704
|
context?: number;
|
|
595
|
-
|
|
705
|
+
multiline?: boolean;
|
|
596
706
|
output_mode?: string;
|
|
707
|
+
limit?: number;
|
|
708
|
+
offset?: number;
|
|
597
709
|
}
|
|
598
710
|
|
|
599
711
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
@@ -607,15 +719,15 @@ export const grepToolRenderer = {
|
|
|
607
719
|
if (args.glob) meta.push(`glob:${args.glob}`);
|
|
608
720
|
if (args.type) meta.push(`type:${args.type}`);
|
|
609
721
|
if (args.output_mode && args.output_mode !== "files_with_matches") meta.push(`mode:${args.output_mode}`);
|
|
610
|
-
if (args.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
if (args.
|
|
722
|
+
if (args.i) meta.push("case:insensitive");
|
|
723
|
+
if (args.n === false) meta.push("no-line-numbers");
|
|
724
|
+
const contextValue = args.context ?? args.c;
|
|
725
|
+
if (contextValue !== undefined && contextValue > 0) meta.push(`context:${contextValue}`);
|
|
726
|
+
if (args.a !== undefined && args.a > 0) meta.push(`after:${args.a}`);
|
|
727
|
+
if (args.b !== undefined && args.b > 0) meta.push(`before:${args.b}`);
|
|
616
728
|
if (args.multiline) meta.push("multiline");
|
|
617
|
-
if (args.
|
|
618
|
-
if (args.
|
|
729
|
+
if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
|
|
730
|
+
if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
|
|
619
731
|
|
|
620
732
|
const text = renderStatusLine(
|
|
621
733
|
{ icon: "pending", title: "Grep", description: args.pattern || "?", meta },
|
|
@@ -669,12 +781,7 @@ export const grepToolRenderer = {
|
|
|
669
781
|
const truncation = details?.meta?.truncation;
|
|
670
782
|
const limits = details?.meta?.limits;
|
|
671
783
|
const truncated = Boolean(
|
|
672
|
-
details?.truncated ||
|
|
673
|
-
truncation ||
|
|
674
|
-
limits?.matchLimit ||
|
|
675
|
-
limits?.resultLimit ||
|
|
676
|
-
limits?.headLimit ||
|
|
677
|
-
limits?.columnTruncated,
|
|
784
|
+
details?.truncated || truncation || limits?.matchLimit || limits?.resultLimit || limits?.columnTruncated,
|
|
678
785
|
);
|
|
679
786
|
const files = details?.files ?? [];
|
|
680
787
|
|
|
@@ -734,7 +841,6 @@ export const grepToolRenderer = {
|
|
|
734
841
|
const truncationReasons: string[] = [];
|
|
735
842
|
if (limits?.matchLimit) truncationReasons.push(`limit ${limits.matchLimit.reached} matches`);
|
|
736
843
|
if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
|
|
737
|
-
if (limits?.headLimit) truncationReasons.push(`head limit ${limits.headLimit.reached}`);
|
|
738
844
|
if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
|
|
739
845
|
if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
|
|
740
846
|
if (truncation?.artifactId) truncationReasons.push(`full output: artifact://${truncation.artifactId}`);
|
package/src/tools/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { PromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
|
|
3
|
-
import type { Skill } from "@oh-my-pi/pi-coding-agent/extensibility/skills";
|
|
4
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { PromptTemplate } from "../config/prompt-templates";
|
|
5
4
|
import type { BashInterceptorRule } from "../config/settings-manager";
|
|
5
|
+
import type { Skill } from "../extensibility/skills";
|
|
6
6
|
import type { InternalUrlRouter } from "../internal-urls";
|
|
7
7
|
import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
|
|
8
8
|
import { checkPythonKernelAvailability } from "../ipy/kernel";
|
|
@@ -144,9 +144,9 @@ export interface ToolSession {
|
|
|
144
144
|
/** Get the current session model string, regardless of how it was chosen */
|
|
145
145
|
getActiveModelString?: () => string | undefined;
|
|
146
146
|
/** Auth storage for passing to subagents (avoids re-discovery) */
|
|
147
|
-
authStorage?: import("
|
|
147
|
+
authStorage?: import("../session/auth-storage").AuthStorage;
|
|
148
148
|
/** Model registry for passing to subagents (avoids re-discovery) */
|
|
149
|
-
modelRegistry?: import("
|
|
149
|
+
modelRegistry?: import("../config/model-registry").ModelRegistry;
|
|
150
150
|
/** MCP manager for proxying MCP calls through parent */
|
|
151
151
|
mcpManager?: import("../mcp/manager").MCPManager;
|
|
152
152
|
/** Internal URL router for agent:// and skill:// URLs */
|
|
@@ -155,7 +155,7 @@ export interface ToolSession {
|
|
|
155
155
|
agentOutputManager?: AgentOutputManager;
|
|
156
156
|
/** Settings manager for passing to subagents (avoids SQLite access in workers) */
|
|
157
157
|
settingsManager?: {
|
|
158
|
-
serialize: () => import("
|
|
158
|
+
serialize: () => import("../config/settings-manager").Settings;
|
|
159
159
|
getPlansDirectory: (cwd?: string) => string;
|
|
160
160
|
};
|
|
161
161
|
/** Settings manager (optional) */
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -250,7 +250,7 @@ export class OutputMetaBuilder {
|
|
|
250
250
|
return this;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/** Add
|
|
253
|
+
/** Add limit notice for head truncation. No-op if reached <= 0. */
|
|
254
254
|
headLimit(reached: number, suggestion = reached * 2): this {
|
|
255
255
|
if (reached <= 0) return this;
|
|
256
256
|
this.#meta.limits = { ...this.#meta.limits, headLimit: { reached, suggestion } };
|
|
@@ -349,7 +349,7 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
349
349
|
}
|
|
350
350
|
if (meta.limits?.headLimit) {
|
|
351
351
|
const l = meta.limits.headLimit;
|
|
352
|
-
parts.push(`${l.reached} results limit reached. Use
|
|
352
|
+
parts.push(`${l.reached} results limit reached. Use limit=${l.suggestion} for more`);
|
|
353
353
|
}
|
|
354
354
|
if (meta.limits?.columnTruncated) {
|
|
355
355
|
parts.push(`Some lines truncated to ${meta.limits.columnTruncated.maxColumn} chars`);
|
package/src/tools/python.ts
CHANGED
|
@@ -172,10 +172,8 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
const { cells, timeout: rawTimeout = 30, cwd, reset } = params;
|
|
175
|
-
// Auto-convert milliseconds to seconds if value > 1000 (16+ min is unreasonable)
|
|
176
|
-
let timeoutSec = rawTimeout > 1000 ? rawTimeout / 1000 : rawTimeout;
|
|
177
175
|
// Clamp to reasonable range: 1s - 600s (10 min)
|
|
178
|
-
timeoutSec = Math.max(1, Math.min(600,
|
|
176
|
+
const timeoutSec = Math.max(1, Math.min(600, rawTimeout));
|
|
179
177
|
const timeoutMs = timeoutSec * 1000;
|
|
180
178
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
181
179
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|