summaryrefslogtreecommitdiff
path: root/packages/core/src
diff options
context:
space:
mode:
authorTommaso Sciortino <[email protected]>2025-06-12 19:46:00 -0700
committerGitHub <[email protected]>2025-06-12 19:46:00 -0700
commit28e656f882ac7ea0f1445f7cd2fa21636ec4744f (patch)
treee5d9f0623b624446cf3c773cece3b41345e05d07 /packages/core/src
parent1c7774e35b06b89788f3ba2083a9b83b743aca1c (diff)
Improve some tools to support abortSignal (#997)
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/services/fileDiscoveryService.ts10
-rw-r--r--packages/core/src/tools/glob.test.ts47
-rw-r--r--packages/core/src/tools/glob.ts51
-rw-r--r--packages/core/src/tools/grep.ts13
-rw-r--r--packages/core/src/tools/read-many-files.ts17
5 files changed, 66 insertions, 72 deletions
diff --git a/packages/core/src/services/fileDiscoveryService.ts b/packages/core/src/services/fileDiscoveryService.ts
index f117813d..7531f90a 100644
--- a/packages/core/src/services/fileDiscoveryService.ts
+++ b/packages/core/src/services/fileDiscoveryService.ts
@@ -7,7 +7,7 @@
import { GitIgnoreParser, GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
import { isGitRepository } from '../utils/gitUtils.js';
import * as path from 'path';
-import fg from 'fast-glob';
+import { glob, type GlobOptions } from 'glob';
export interface FileDiscoveryOptions {
respectGitIgnore?: boolean;
@@ -35,12 +35,12 @@ export class FileDiscoveryService {
async glob(
pattern: string | string[],
- options: fg.Options = {},
+ options: GlobOptions = {},
): Promise<string[]> {
- const files = await fg(pattern, {
+ const files = (await glob(pattern, {
...options,
- caseSensitiveMatch: false,
- });
+ nocase: true,
+ })) as string[];
return this.filterFiles(files);
}
diff --git a/packages/core/src/tools/glob.test.ts b/packages/core/src/tools/glob.test.ts
index 90fe1a2e..37738ba7 100644
--- a/packages/core/src/tools/glob.test.ts
+++ b/packages/core/src/tools/glob.test.ts
@@ -4,17 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import {
- GlobTool,
- GlobToolParams,
- sortFileEntries,
- GlobFileEntry,
-} from './glob.js';
+import { GlobTool, GlobToolParams, GlobPath, sortFileEntries } from './glob.js';
import { partListUnionToString } from '../core/geminiRequest.js';
-// import { ToolResult } from './tools.js'; // ToolResult is implicitly used by execute
import path from 'path';
import fs from 'fs/promises';
-import { Stats } from 'fs';
import os from 'os';
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; // Removed vi
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
@@ -274,9 +267,9 @@ describe('sortFileEntries', () => {
const nowTimestamp = new Date('2024-01-15T12:00:00.000Z').getTime();
const oneDayInMs = 24 * 60 * 60 * 1000;
- const createFileEntry = (path: string, mtimeDate: Date): GlobFileEntry => ({
- path,
- stats: { mtime: mtimeDate } as Stats,
+ const createFileEntry = (fullpath: string, mtimeDate: Date): GlobPath => ({
+ fullpath: () => fullpath,
+ mtimeMs: mtimeDate.getTime(),
});
it('should sort a mix of recent and older files correctly', () => {
@@ -289,7 +282,7 @@ describe('sortFileEntries', () => {
nowTimestamp - (oneDayInMs + 2 * 60 * 60 * 1000),
); // 26 hours ago
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('older_zebra.txt', olderTime2),
createFileEntry('recent_alpha.txt', recentTime1),
createFileEntry('older_apple.txt', olderTime1),
@@ -298,7 +291,7 @@ describe('sortFileEntries', () => {
];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
- const sortedPaths = sorted.map((e) => e.path);
+ const sortedPaths = sorted.map((e) => e.fullpath());
expect(sortedPaths).toEqual([
'recent_alpha.txt', // Recent, newest
@@ -314,24 +307,28 @@ describe('sortFileEntries', () => {
const recentTime2 = new Date(nowTimestamp - 2000);
const recentTime3 = new Date(nowTimestamp - 3000); // Oldest recent
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('c.txt', recentTime2),
createFileEntry('a.txt', recentTime3),
createFileEntry('b.txt', recentTime1),
];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
- expect(sorted.map((e) => e.path)).toEqual(['b.txt', 'c.txt', 'a.txt']);
+ expect(sorted.map((e) => e.fullpath())).toEqual([
+ 'b.txt',
+ 'c.txt',
+ 'a.txt',
+ ]);
});
it('should sort only older files alphabetically by path', () => {
const olderTime = new Date(nowTimestamp - 2 * oneDayInMs); // All equally old
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('zebra.txt', olderTime),
createFileEntry('apple.txt', olderTime),
createFileEntry('banana.txt', olderTime),
];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
- expect(sorted.map((e) => e.path)).toEqual([
+ expect(sorted.map((e) => e.fullpath())).toEqual([
'apple.txt',
'banana.txt',
'zebra.txt',
@@ -339,30 +336,30 @@ describe('sortFileEntries', () => {
});
it('should handle an empty array', () => {
- const entries: GlobFileEntry[] = [];
+ const entries: GlobPath[] = [];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
expect(sorted).toEqual([]);
});
it('should correctly sort files when mtimes are identical for older files', () => {
const olderTime = new Date(nowTimestamp - 2 * oneDayInMs);
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('b.txt', olderTime),
createFileEntry('a.txt', olderTime),
];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
- expect(sorted.map((e) => e.path)).toEqual(['a.txt', 'b.txt']);
+ expect(sorted.map((e) => e.fullpath())).toEqual(['a.txt', 'b.txt']);
});
it('should correctly sort files when mtimes are identical for recent files (maintaining mtime sort)', () => {
const recentTime = new Date(nowTimestamp - 1000);
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('b.txt', recentTime),
createFileEntry('a.txt', recentTime),
];
const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
- expect(sorted.map((e) => e.path)).toContain('a.txt');
- expect(sorted.map((e) => e.path)).toContain('b.txt');
+ expect(sorted.map((e) => e.fullpath())).toContain('a.txt');
+ expect(sorted.map((e) => e.fullpath())).toContain('b.txt');
expect(sorted.length).toBe(2);
});
@@ -371,12 +368,12 @@ describe('sortFileEntries', () => {
const justUnderThreshold = new Date(nowTimestamp - (1000 - 1)); // Barely recent
const customThresholdMs = 1000; // 1 second
- const entries: GlobFileEntry[] = [
+ const entries: GlobPath[] = [
createFileEntry('older_file.txt', justOverThreshold),
createFileEntry('recent_file.txt', justUnderThreshold),
];
const sorted = sortFileEntries(entries, nowTimestamp, customThresholdMs);
- expect(sorted.map((e) => e.path)).toEqual([
+ expect(sorted.map((e) => e.fullpath())).toEqual([
'recent_file.txt',
'older_file.txt',
]);
diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts
index 6acb2a2b..d94a380a 100644
--- a/packages/core/src/tools/glob.ts
+++ b/packages/core/src/tools/glob.ts
@@ -6,16 +6,16 @@
import fs from 'fs';
import path from 'path';
-import fg from 'fast-glob';
+import { glob } from 'glob';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { BaseTool, ToolResult } from './tools.js';
import { shortenPath, makeRelative } from '../utils/paths.js';
import { Config } from '../config/config.js';
-// Type definition for file entries returned by fast-glob with stats: true
-export interface GlobFileEntry {
- path: string;
- stats?: fs.Stats;
+// Subset of 'Path' interface provided by 'glob' that we can implement for testing
+export interface GlobPath {
+ fullpath(): string;
+ mtimeMs?: number;
}
/**
@@ -24,14 +24,14 @@ export interface GlobFileEntry {
* Older files are listed after recent ones, sorted alphabetically by path.
*/
export function sortFileEntries(
- entries: GlobFileEntry[],
+ entries: GlobPath[],
nowTimestamp: number,
recencyThresholdMs: number,
-): GlobFileEntry[] {
+): GlobPath[] {
const sortedEntries = [...entries];
sortedEntries.sort((a, b) => {
- const mtimeA = a.stats?.mtime?.getTime() ?? 0;
- const mtimeB = b.stats?.mtime?.getTime() ?? 0;
+ const mtimeA = a.mtimeMs ?? 0;
+ const mtimeB = b.mtimeMs ?? 0;
const aIsRecent = nowTimestamp - mtimeA < recencyThresholdMs;
const bIsRecent = nowTimestamp - mtimeB < recencyThresholdMs;
@@ -42,7 +42,7 @@ export function sortFileEntries(
} else if (bIsRecent) {
return 1;
} else {
- return a.path.localeCompare(b.path);
+ return a.fullpath().localeCompare(b.fullpath());
}
});
return sortedEntries;
@@ -201,7 +201,7 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
*/
async execute(
params: GlobToolParams,
- _signal: AbortSignal,
+ signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateToolParams(params);
if (validationError) {
@@ -223,26 +223,25 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
this.config.getFileFilteringRespectGitIgnore();
const fileDiscovery = await this.config.getFileService();
- const entries = await fg(params.pattern, {
+ const entries = (await glob(params.pattern, {
cwd: searchDirAbsolute,
- absolute: true,
- onlyFiles: true,
- stats: true,
+ withFileTypes: true,
+ nodir: true,
+ stat: true,
+ nocase: !params.case_sensitive,
dot: true,
- caseSensitiveMatch: params.case_sensitive ?? false,
ignore: ['**/node_modules/**', '**/.git/**'],
- followSymbolicLinks: false,
- suppressErrors: true,
- });
+ follow: false,
+ signal,
+ })) as GlobPath[];
// Apply git-aware filtering if enabled and in git repository
let filteredEntries = entries;
let gitIgnoredCount = 0;
if (respectGitIgnore && fileDiscovery.isGitRepository()) {
- const allPaths = entries.map((entry) => entry.path);
- const relativePaths = allPaths.map((p) =>
- path.relative(this.rootDirectory, p),
+ const relativePaths = entries.map((p) =>
+ path.relative(this.rootDirectory, p.fullpath()),
);
const filteredRelativePaths = fileDiscovery.filterFiles(relativePaths, {
respectGitIgnore,
@@ -252,7 +251,7 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
);
filteredEntries = entries.filter((entry) =>
- filteredAbsolutePaths.has(entry.path),
+ filteredAbsolutePaths.has(entry.fullpath()),
);
gitIgnoredCount = entries.length - filteredEntries.length;
}
@@ -274,12 +273,14 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
// Sort the filtered entries using the new helper function
const sortedEntries = sortFileEntries(
- filteredEntries as GlobFileEntry[], // Cast because fast-glob's Entry type is generic
+ filteredEntries,
nowTimestamp,
oneDayInMs,
);
- const sortedAbsolutePaths = sortedEntries.map((entry) => entry.path);
+ const sortedAbsolutePaths = sortedEntries.map((entry) =>
+ entry.fullpath(),
+ );
const fileListDescription = sortedAbsolutePaths.join('\n');
const fileCount = sortedAbsolutePaths.length;
diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts
index 38a1d4f5..43ec579a 100644
--- a/packages/core/src/tools/grep.ts
+++ b/packages/core/src/tools/grep.ts
@@ -9,7 +9,7 @@ import fsPromises from 'fs/promises';
import path from 'path';
import { EOL } from 'os';
import { spawn } from 'child_process';
-import fastGlob from 'fast-glob';
+import { globStream } from 'glob';
import { BaseTool, ToolResult } from './tools.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js';
@@ -168,7 +168,7 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> {
*/
async execute(
params: GrepToolParams,
- _signal: AbortSignal,
+ signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateToolParams(params);
if (validationError) {
@@ -187,6 +187,7 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> {
pattern: params.pattern,
path: searchDirAbs,
include: params.include,
+ signal,
});
if (matches.length === 0) {
@@ -382,6 +383,7 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> {
pattern: string;
path: string; // Expects absolute path
include?: string;
+ signal: AbortSignal;
}): Promise<GrepMatch[]> {
const { pattern, path: absolutePath, include } = options;
let strategyUsed = 'none';
@@ -533,14 +535,13 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> {
'.hg/**',
]; // Use glob patterns for ignores here
- const filesStream = fastGlob.stream(globPattern, {
+ const filesStream = globStream(globPattern, {
cwd: absolutePath,
dot: true,
ignore: ignorePatterns,
absolute: true,
- onlyFiles: true,
- suppressErrors: true,
- stats: false,
+ nodir: true,
+ signal: options.signal,
});
const regex = new RegExp(pattern, 'i');
diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts
index 718ce510..daf9bc04 100644
--- a/packages/core/src/tools/read-many-files.ts
+++ b/packages/core/src/tools/read-many-files.ts
@@ -8,7 +8,7 @@ import { BaseTool, ToolResult } from './tools.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { getErrorMessage } from '../utils/errors.js';
import * as path from 'path';
-import fg from 'fast-glob';
+import { glob } from 'glob';
import { getCurrentGeminiMdFilename } from './memoryTool.js';
import {
detectFileType,
@@ -270,7 +270,7 @@ Use this tool when the user's query implies needing the content of several files
async execute(
params: ReadManyFilesParams,
- _signal: AbortSignal,
+ signal: AbortSignal,
): Promise<ToolResult> {
const validationError = this.validateParams(params);
if (validationError) {
@@ -313,19 +313,14 @@ Use this tool when the user's query implies needing the content of several files
}
try {
- // Using fast-glob (fg) for file searching based on patterns.
- // The `cwd` option scopes the search to the toolBaseDir.
- // `ignore` handles exclusions.
- // `onlyFiles` ensures only files are returned.
- // `dot` allows matching dotfiles (which can still be excluded by patterns).
- // `absolute` returns absolute paths for consistent handling.
- const entries = await fg(searchPatterns, {
+ const entries = await glob(searchPatterns, {
cwd: toolBaseDir,
ignore: effectiveExcludes,
- onlyFiles: true,
+ nodir: true,
dot: true,
absolute: true,
- caseSensitiveMatch: false,
+ nocase: true,
+ signal,
});
// Apply git-aware filtering if enabled and in git repository