summaryrefslogtreecommitdiff
path: root/packages/core/src/tools
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/tools')
-rw-r--r--packages/core/src/tools/glob.test.ts18
-rw-r--r--packages/core/src/tools/glob.ts64
-rw-r--r--packages/core/src/tools/ls.ts54
-rw-r--r--packages/core/src/tools/read-many-files.test.ts16
-rw-r--r--packages/core/src/tools/read-many-files.ts61
-rw-r--r--packages/core/src/tools/shell.ts27
6 files changed, 220 insertions, 20 deletions
diff --git a/packages/core/src/tools/glob.test.ts b/packages/core/src/tools/glob.test.ts
index d42e5b1c..b630a0d8 100644
--- a/packages/core/src/tools/glob.test.ts
+++ b/packages/core/src/tools/glob.test.ts
@@ -11,16 +11,30 @@ import path from 'path';
import fs from 'fs/promises';
import os from 'os';
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; // Removed vi
+import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
+import { Config } from '../config/config.js';
describe('GlobTool', () => {
let tempRootDir: string; // This will be the rootDirectory for the GlobTool instance
let globTool: GlobTool;
const abortSignal = new AbortController().signal;
+ // Mock config for testing
+ const mockConfig = {
+ getFileService: async () => {
+ const service = new FileDiscoveryService(tempRootDir);
+ await service.initialize({ respectGitIgnore: true });
+ return service;
+ },
+ getFileFilteringRespectGitIgnore: () => true,
+ getFileFilteringCustomIgnorePatterns: () => [],
+ getFileFilteringAllowBuildArtifacts: () => false,
+ } as Partial<Config> as Config;
+
beforeEach(async () => {
// Create a unique root directory for each test run
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'glob-tool-root-'));
- globTool = new GlobTool(tempRootDir);
+ globTool = new GlobTool(tempRootDir, mockConfig);
// Create some test files and directories within this root
// Top-level files
@@ -214,7 +228,7 @@ describe('GlobTool', () => {
it("should return error if search path resolves outside the tool's root directory", () => {
// Create a globTool instance specifically for this test, with a deeper root
const deeperRootDir = path.join(tempRootDir, 'sub');
- const specificGlobTool = new GlobTool(deeperRootDir);
+ const specificGlobTool = new GlobTool(deeperRootDir, mockConfig);
// const params: GlobToolParams = { pattern: '*.txt', path: '..' }; // This line is unused and will be removed.
// This should be fine as tempRootDir is still within the original tempRootDir (the parent of deeperRootDir)
// Let's try to go further up.
diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts
index 86aef44f..d4b479eb 100644
--- a/packages/core/src/tools/glob.ts
+++ b/packages/core/src/tools/glob.ts
@@ -10,6 +10,7 @@ import fg from 'fast-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';
/**
* Parameters for the GlobTool
@@ -29,6 +30,11 @@ export interface GlobToolParams {
* Whether the search should be case-sensitive (optional, defaults to false)
*/
case_sensitive?: boolean;
+
+ /**
+ * Whether to respect .gitignore patterns (optional, defaults to true)
+ */
+ respect_git_ignore?: boolean;
}
/**
@@ -40,7 +46,10 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
* Creates a new instance of the GlobLogic
* @param rootDirectory Root directory to ground this tool in.
*/
- constructor(private rootDirectory: string) {
+ constructor(
+ private rootDirectory: string,
+ private config: Config,
+ ) {
super(
GlobTool.Name,
'FindFiles',
@@ -62,6 +71,11 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
'Optional: Whether the search should be case-sensitive. Defaults to false.',
type: 'boolean',
},
+ respect_git_ignore: {
+ description:
+ 'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.',
+ type: 'boolean',
+ },
},
required: ['pattern'],
type: 'object',
@@ -167,6 +181,12 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
params.path || '.',
);
+ // Get centralized file discovery service
+ const respectGitIgnore =
+ params.respect_git_ignore ??
+ this.config.getFileFilteringRespectGitIgnore();
+ const fileDiscovery = await this.config.getFileService();
+
const entries = await fg(params.pattern, {
cwd: searchDirAbsolute,
absolute: true,
@@ -179,25 +199,57 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
suppressErrors: true,
});
- if (!entries || entries.length === 0) {
+ // 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 filteredRelativePaths = fileDiscovery.filterFiles(relativePaths, {
+ respectGitIgnore,
+ });
+ const filteredAbsolutePaths = new Set(
+ filteredRelativePaths.map((p) => path.resolve(this.rootDirectory, p)),
+ );
+
+ filteredEntries = entries.filter((entry) =>
+ filteredAbsolutePaths.has(entry.path),
+ );
+ gitIgnoredCount = entries.length - filteredEntries.length;
+ }
+
+ if (!filteredEntries || filteredEntries.length === 0) {
+ let message = `No files found matching pattern "${params.pattern}" within ${searchDirAbsolute}.`;
+ if (gitIgnoredCount > 0) {
+ message += ` (${gitIgnoredCount} files were git-ignored)`;
+ }
return {
- llmContent: `No files found matching pattern "${params.pattern}" within ${searchDirAbsolute}.`,
+ llmContent: message,
returnDisplay: `No files found`,
};
}
- entries.sort((a, b) => {
+ filteredEntries.sort((a, b) => {
const mtimeA = a.stats?.mtime?.getTime() ?? 0;
const mtimeB = b.stats?.mtime?.getTime() ?? 0;
return mtimeB - mtimeA;
});
- const sortedAbsolutePaths = entries.map((entry) => entry.path);
+ const sortedAbsolutePaths = filteredEntries.map((entry) => entry.path);
const fileListDescription = sortedAbsolutePaths.join('\n');
const fileCount = sortedAbsolutePaths.length;
+ let resultMessage = `Found ${fileCount} file(s) matching "${params.pattern}" within ${searchDirAbsolute}`;
+ if (gitIgnoredCount > 0) {
+ resultMessage += ` (${gitIgnoredCount} additional files were git-ignored)`;
+ }
+ resultMessage += `, sorted by modification time (newest first):\n${fileListDescription}`;
+
return {
- llmContent: `Found ${fileCount} file(s) matching "${params.pattern}" within ${searchDirAbsolute}, sorted by modification time (newest first):\n${fileListDescription}`,
+ llmContent: resultMessage,
returnDisplay: `Found ${fileCount} matching file(s)`,
};
} catch (error) {
diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts
index fea95187..56a016aa 100644
--- a/packages/core/src/tools/ls.ts
+++ b/packages/core/src/tools/ls.ts
@@ -9,6 +9,7 @@ import path from 'path';
import { BaseTool, ToolResult } from './tools.js';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { makeRelative, shortenPath } from '../utils/paths.js';
+import { Config } from '../config/config.js';
/**
* Parameters for the LS tool
@@ -20,9 +21,14 @@ export interface LSToolParams {
path: string;
/**
- * List of glob patterns to ignore
+ * Array of glob patterns to ignore (optional)
*/
ignore?: string[];
+
+ /**
+ * Whether to respect .gitignore patterns (optional, defaults to true)
+ */
+ respect_git_ignore?: boolean;
}
/**
@@ -65,7 +71,10 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
* Creates a new instance of the LSLogic
* @param rootDirectory Root directory to ground this tool in. All operations will be restricted to this directory.
*/
- constructor(private rootDirectory: string) {
+ constructor(
+ private rootDirectory: string,
+ private config: Config,
+ ) {
super(
LSTool.Name,
'ReadFolder',
@@ -84,6 +93,11 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
},
type: 'array',
},
+ respect_git_ignore: {
+ description:
+ 'Optional: Whether to respect .gitignore patterns when listing files. Only available in git repositories. Defaults to true.',
+ type: 'boolean',
+ },
},
required: ['path'],
type: 'object',
@@ -214,7 +228,16 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
}
const files = fs.readdirSync(params.path);
+
+ // Get centralized file discovery service
+ const respectGitIgnore =
+ params.respect_git_ignore ??
+ this.config.getFileFilteringRespectGitIgnore();
+ const fileDiscovery = await this.config.getFileService();
+
const entries: FileEntry[] = [];
+ let gitIgnoredCount = 0;
+
if (files.length === 0) {
// Changed error message to be more neutral for LLM
return {
@@ -229,6 +252,18 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
}
const fullPath = path.join(params.path, file);
+ const relativePath = path.relative(this.rootDirectory, fullPath);
+
+ // Check if this file should be git-ignored (only in git repositories)
+ if (
+ respectGitIgnore &&
+ fileDiscovery.isGitRepository() &&
+ fileDiscovery.shouldIgnoreFile(relativePath)
+ ) {
+ gitIgnoredCount++;
+ continue;
+ }
+
try {
const stats = fs.statSync(fullPath);
const isDir = stats.isDirectory();
@@ -257,10 +292,19 @@ export class LSTool extends BaseTool<LSToolParams, ToolResult> {
.map((entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`)
.join('\n');
+ let resultMessage = `Directory listing for ${params.path}:\n${directoryContent}`;
+ if (gitIgnoredCount > 0) {
+ resultMessage += `\n\n(${gitIgnoredCount} items were git-ignored)`;
+ }
+
+ let displayMessage = `Listed ${entries.length} item(s).`;
+ if (gitIgnoredCount > 0) {
+ displayMessage += ` (${gitIgnoredCount} git-ignored)`;
+ }
+
return {
- llmContent: `Directory listing for ${params.path}:\n${directoryContent}`,
- // Simplified display, CLI wrapper can enhance
- returnDisplay: `Listed ${entries.length} item(s).`,
+ llmContent: resultMessage,
+ returnDisplay: displayMessage,
};
} catch (error) {
const errorMsg = `Error listing directory: ${error instanceof Error ? error.message : String(error)}`;
diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts
index 5c6d94fa..f4ecc9d0 100644
--- a/packages/core/src/tools/read-many-files.test.ts
+++ b/packages/core/src/tools/read-many-files.test.ts
@@ -9,6 +9,8 @@ import type { Mock } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mockControl } from '../__mocks__/fs/promises.js';
import { ReadManyFilesTool } from './read-many-files.js';
+import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
+import { Config } from '../config/config.js';
import path from 'path';
import fs from 'fs'; // Actual fs for setup
import os from 'os';
@@ -19,6 +21,18 @@ describe('ReadManyFilesTool', () => {
let tempDirOutsideRoot: string;
let mockReadFileFn: Mock;
+ // Mock config for testing
+ const mockConfig = {
+ getFileService: async () => {
+ const service = new FileDiscoveryService(tempRootDir);
+ await service.initialize({ respectGitIgnore: true });
+ return service;
+ },
+ getFileFilteringRespectGitIgnore: () => true,
+ getFileFilteringCustomIgnorePatterns: () => [],
+ getFileFilteringAllowBuildArtifacts: () => false,
+ } as Partial<Config> as Config;
+
beforeEach(async () => {
tempRootDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'read-many-files-root-'),
@@ -26,7 +40,7 @@ describe('ReadManyFilesTool', () => {
tempDirOutsideRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'read-many-files-external-'),
);
- tool = new ReadManyFilesTool(tempRootDir);
+ tool = new ReadManyFilesTool(tempRootDir, mockConfig);
mockReadFileFn = mockControl.mockReadFile;
mockReadFileFn.mockReset();
diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts
index 4ba09ef0..30f70c91 100644
--- a/packages/core/src/tools/read-many-files.ts
+++ b/packages/core/src/tools/read-many-files.ts
@@ -16,6 +16,7 @@ import {
DEFAULT_ENCODING,
} from '../utils/fileUtils.js';
import { PartListUnion } from '@google/genai';
+import { Config } from '../config/config.js';
/**
* Parameters for the ReadManyFilesTool.
@@ -54,6 +55,11 @@ export interface ReadManyFilesParams {
* Optional. Apply default exclusion patterns. Defaults to true.
*/
useDefaultExcludes?: boolean;
+
+ /**
+ * Optional. Whether to respect .gitignore patterns. Defaults to true.
+ */
+ respect_git_ignore?: boolean;
}
/**
@@ -119,7 +125,10 @@ export class ReadManyFilesTool extends BaseTool<
* @param targetDir The absolute root directory within which this tool is allowed to operate.
* All paths provided in `params` will be resolved relative to this directory.
*/
- constructor(readonly targetDir: string) {
+ constructor(
+ readonly targetDir: string,
+ private config: Config,
+ ) {
const parameterSchema: Record<string, unknown> = {
type: 'object',
properties: {
@@ -155,6 +164,12 @@ export class ReadManyFilesTool extends BaseTool<
'Optional. Whether to apply a list of default exclusion patterns (e.g., node_modules, .git, binary files). Defaults to true.',
default: true,
},
+ respect_git_ignore: {
+ type: 'boolean',
+ description:
+ 'Optional. Whether to respect .gitignore patterns when discovering files. Only available in git repositories. Defaults to true.',
+ default: true,
+ },
},
required: ['paths'],
};
@@ -254,8 +269,15 @@ Use this tool when the user's query implies needing the content of several files
include = [],
exclude = [],
useDefaultExcludes = true,
+ respect_git_ignore = true,
} = params;
+ const respectGitIgnore =
+ respect_git_ignore ?? this.config.getFileFilteringRespectGitIgnore();
+
+ // Get centralized file discovery service
+ const fileDiscovery = await this.config.getFileService();
+
const toolBaseDir = this.targetDir;
const filesToConsider = new Set<string>();
const skippedFiles: Array<{ path: string; reason: string }> = [];
@@ -290,9 +312,22 @@ Use this tool when the user's query implies needing the content of several files
caseSensitiveMatch: false,
});
+ // Apply git-aware filtering if enabled and in git repository
+ const filteredEntries =
+ respectGitIgnore && fileDiscovery.isGitRepository()
+ ? fileDiscovery
+ .filterFiles(
+ entries.map((p) => path.relative(toolBaseDir, p)),
+ {
+ respectGitIgnore,
+ },
+ )
+ .map((p) => path.resolve(toolBaseDir, p))
+ : entries;
+
+ let gitIgnoredCount = 0;
for (const absoluteFilePath of entries) {
// Security check: ensure the glob library didn't return something outside targetDir.
- // This should be guaranteed by `cwd` and the library's sandboxing, but an extra check is good practice.
if (!absoluteFilePath.startsWith(toolBaseDir)) {
skippedFiles.push({
path: absoluteFilePath,
@@ -300,8 +335,30 @@ Use this tool when the user's query implies needing the content of several files
});
continue;
}
+
+ // Check if this file was filtered out by git ignore
+ if (
+ respectGitIgnore &&
+ fileDiscovery.isGitRepository() &&
+ !filteredEntries.includes(absoluteFilePath)
+ ) {
+ gitIgnoredCount++;
+ continue;
+ }
+
filesToConsider.add(absoluteFilePath);
}
+
+ // Add info about git-ignored files if any were filtered
+ if (gitIgnoredCount > 0) {
+ const reason = respectGitIgnore
+ ? 'git-ignored'
+ : 'filtered by custom ignore patterns';
+ skippedFiles.push({
+ path: `${gitIgnoredCount} file(s)`,
+ reason,
+ });
+ }
} catch (error) {
return {
llmContent: `Error during file search: ${getErrorMessage(error)}`,
diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts
index 53e2bbf3..2117366a 100644
--- a/packages/core/src/tools/shell.ts
+++ b/packages/core/src/tools/shell.ts
@@ -35,10 +35,29 @@ export class ShellTool extends BaseTool<ShellToolParams, ToolResult> {
constructor(private readonly config: Config) {
const toolDisplayName = 'Shell';
- const descriptionUrl = new URL('shell.md', import.meta.url);
- const toolDescription = fs.readFileSync(descriptionUrl, 'utf-8');
- const schemaUrl = new URL('shell.json', import.meta.url);
- const toolParameterSchema = JSON.parse(fs.readFileSync(schemaUrl, 'utf-8'));
+
+ let toolDescription: string;
+ let toolParameterSchema: Record<string, unknown>;
+
+ try {
+ const descriptionUrl = new URL('shell.md', import.meta.url);
+ toolDescription = fs.readFileSync(descriptionUrl, 'utf-8');
+ const schemaUrl = new URL('shell.json', import.meta.url);
+ toolParameterSchema = JSON.parse(fs.readFileSync(schemaUrl, 'utf-8'));
+ } catch {
+ // Fallback with minimal descriptions for tests when file reading fails
+ toolDescription = 'Execute shell commands';
+ toolParameterSchema = {
+ type: 'object',
+ properties: {
+ command: { type: 'string', description: 'Command to execute' },
+ description: { type: 'string', description: 'Command description' },
+ directory: { type: 'string', description: 'Working directory' },
+ },
+ required: ['command'],
+ };
+ }
+
super(
ShellTool.Name,
toolDisplayName,