@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/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: "Search pattern (regex)" }),
27
- path: Type.Optional(Type.String({ description: "Directory or file to search (default: cwd)" })),
28
- glob: Type.Optional(Type.String({ description: "Glob filter, e.g. '*.ts', '**/*.spec.ts'" })),
29
- type: Type.Optional(Type.String({ description: "File type filter, e.g. 'ts', 'py', 'rust'" })),
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(["content", "files_with_matches", "count"], {
38
- description: "Output format (default: content)",
31
+ StringEnum(["files_with_matches", "content", "count"], {
32
+ description: "Output format (default: files_with_matches)",
39
33
  }),
40
34
  ),
41
- head_limit: Type.Optional(Type.Number({ description: "Truncate output to first N results" })),
42
- offset: Type.Optional(Type.Number({ description: "Skip first N results (default: 0)" })),
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 DEFAULT_LIMIT = 100;
46
+ const DEFAULT_MATCH_LIMIT = 100;
46
47
 
47
48
  export interface GrepToolDetails {
48
49
  truncation?: TruncationResult;
49
50
  matchLimitReached?: number;
50
- headLimitReached?: number;
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
- ignore_case?: boolean;
91
- case_sensitive?: boolean;
92
- literal?: boolean;
93
- multiline?: boolean;
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
- ignore_case,
153
- case_sensitive,
154
- literal,
155
- multiline,
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
- // Auto-detect invalid regex patterns and switch to literal mode
165
- // This handles cases like "abort(" which would cause ripgrep regex parse errors
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
- let useLiteral = literal ?? false;
176
- if (!useLiteral) {
177
- const validation = await this.validateRegexPattern(pattern, rgPath);
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 contextValue = context && context > 0 ? context : 0;
197
- const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
198
- const effectiveOutputMode = output_mode ?? "content";
199
- const effectiveOffset = offset && offset > 0 ? offset : 0;
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", "--hidden");
320
+ args.push("--files-with-matches", "--color=never");
233
321
  } else if (effectiveOutputMode === "count") {
234
- args.push("--count", "--color=never", "--hidden");
322
+ args.push("--count", "--color=never");
235
323
  } else {
236
- args.push("--json", "--line-number", "--color=never", "--hidden");
324
+ args.push("--json", "--color=never");
325
+ if (showLineNumbers) {
326
+ args.push("--line-number");
327
+ }
237
328
  }
238
329
 
239
- if (case_sensitive) {
240
- args.push("--case-sensitive");
241
- } else if (ignore_case) {
330
+ args.push("--hidden");
331
+
332
+ if (ignoreCase) {
242
333
  args.push("--ignore-case");
243
334
  } else {
244
- args.push("--smart-case");
335
+ args.push("--case-sensitive");
245
336
  }
246
337
 
247
338
  if (multiline) {
248
339
  args.push("--multiline");
249
340
  }
250
341
 
251
- if (useLiteral) {
252
- args.push("--fixed-strings");
342
+ if (normalizedGlob) {
343
+ args.push("--glob", normalizedGlob);
253
344
  }
254
345
 
255
- if (glob) {
256
- args.push("--glob", glob);
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 (type) {
260
- args.push("--type", type);
353
+ if (effectiveOutputMode === "content") {
354
+ if (normalizedContext > 0) {
355
+ args.push("-C", String(normalizedContext));
356
+ }
261
357
  }
262
358
 
263
- args.push("--", pattern, searchPath);
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: effectiveLimit,
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 lines) {
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 lines) {
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 || limitMeta.headLimit);
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
- headLimitReached: limitMeta.headLimit?.reached,
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
- headLimitReached: limitMeta.headLimit?.reached,
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 [`${relativePath}:${lineNumber}: (unable to read file)`];
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 = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
435
- const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
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(`${relativePath}:${current}: ${truncatedText}`);
543
+ block.push(
544
+ showLineNumbers
545
+ ? `${relativePath}:${current}: ${truncatedText}`
546
+ : `${relativePath}: ${truncatedText}`,
547
+ );
449
548
  } else {
450
- block.push(`${relativePath}-${current}- ${truncatedText}`);
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() || matchCount >= effectiveLimit) {
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
- // Apply offset and head_limit to output lines
535
- let processedLines = effectiveOffset > 0 ? outputLines.slice(effectiveOffset) : outputLines;
536
- const listLimit = applyListLimit(processedLines, { headLimit: head_limit });
537
- processedLines = listLimit.items;
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 = processedLines.join("\n");
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.headLimit || linesTruncated);
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
- headLimitReached: limitMeta.headLimit?.reached,
665
+ matchLimitReached: limitMeta.matchLimit?.reached,
558
666
  };
559
667
 
560
668
  // Keep TUI compatibility fields
561
- if (matchLimitReached) details.matchLimitReached = effectiveLimit;
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: matchLimitReached ? effectiveLimit : undefined,
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
- ignore_case?: boolean;
591
- case_sensitive?: boolean;
592
- literal?: boolean;
593
- multiline?: boolean;
699
+ i?: boolean;
700
+ n?: boolean;
701
+ a?: number;
702
+ b?: number;
703
+ c?: number;
594
704
  context?: number;
595
- limit?: number;
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.case_sensitive) {
611
- meta.push("case:sensitive");
612
- } else if (args.ignore_case) {
613
- meta.push("case:insensitive");
614
- }
615
- if (args.literal) meta.push("literal");
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.context !== undefined) meta.push(`context:${args.context}`);
618
- if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
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}`);
@@ -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("@oh-my-pi/pi-coding-agent/session/auth-storage").AuthStorage;
147
+ authStorage?: import("../session/auth-storage").AuthStorage;
148
148
  /** Model registry for passing to subagents (avoids re-discovery) */
149
- modelRegistry?: import("@oh-my-pi/pi-coding-agent/config/model-registry").ModelRegistry;
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("@oh-my-pi/pi-coding-agent/config/settings-manager").Settings;
158
+ serialize: () => import("../config/settings-manager").Settings;
159
159
  getPlansDirectory: (cwd?: string) => string;
160
160
  };
161
161
  /** Settings manager (optional) */
@@ -250,7 +250,7 @@ export class OutputMetaBuilder {
250
250
  return this;
251
251
  }
252
252
 
253
- /** Add head_limit notice. No-op if reached <= 0. */
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 head_limit=${l.suggestion} for more`);
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`);
@@ -1,4 +1,4 @@
1
- import { resolvePlanUrlToPath } from "@oh-my-pi/pi-coding-agent/internal-urls";
1
+ import { resolvePlanUrlToPath } from "../internal-urls";
2
2
  import type { ToolSession } from ".";
3
3
  import { resolveToCwd } from "./path-utils";
4
4
  import { ToolError } from "./tool-errors";
@@ -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, timeoutSec));
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;