diff options
| author | Tommaso Sciortino <[email protected]> | 2025-06-12 19:46:00 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-12 19:46:00 -0700 |
| commit | 28e656f882ac7ea0f1445f7cd2fa21636ec4744f (patch) | |
| tree | e5d9f0623b624446cf3c773cece3b41345e05d07 /packages/core/src | |
| parent | 1c7774e35b06b89788f3ba2083a9b83b743aca1c (diff) | |
Improve some tools to support abortSignal (#997)
Diffstat (limited to 'packages/core/src')
| -rw-r--r-- | packages/core/src/services/fileDiscoveryService.ts | 10 | ||||
| -rw-r--r-- | packages/core/src/tools/glob.test.ts | 47 | ||||
| -rw-r--r-- | packages/core/src/tools/glob.ts | 51 | ||||
| -rw-r--r-- | packages/core/src/tools/grep.ts | 13 | ||||
| -rw-r--r-- | packages/core/src/tools/read-many-files.ts | 17 |
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 |
