quantum-cli-core
Version:
Quantum CLI Core - Multi-LLM Collaboration System
215 lines • 9.48 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'fs';
import path from 'path';
import { glob } from 'glob';
import { SchemaValidator } from '../utils/schemaValidator.js';
import { BaseTool } from './tools.js';
import { shortenPath, makeRelative } from '../utils/paths.js';
/**
* Sorts file entries based on recency and then alphabetically.
* Recent files (modified within recencyThresholdMs) are listed first, newest to oldest.
* Older files are listed after recent ones, sorted alphabetically by path.
*/
export function sortFileEntries(entries, nowTimestamp, recencyThresholdMs) {
const sortedEntries = [...entries];
sortedEntries.sort((a, b) => {
const mtimeA = a.mtimeMs ?? 0;
const mtimeB = b.mtimeMs ?? 0;
const aIsRecent = nowTimestamp - mtimeA < recencyThresholdMs;
const bIsRecent = nowTimestamp - mtimeB < recencyThresholdMs;
if (aIsRecent && bIsRecent) {
return mtimeB - mtimeA;
}
else if (aIsRecent) {
return -1;
}
else if (bIsRecent) {
return 1;
}
else {
return a.fullpath().localeCompare(b.fullpath());
}
});
return sortedEntries;
}
/**
* Implementation of the Glob tool logic
*/
export class GlobTool extends BaseTool {
rootDirectory;
config;
static Name = 'glob';
/**
* Creates a new instance of the GlobLogic
* @param rootDirectory Root directory to ground this tool in.
*/
constructor(rootDirectory, config) {
super(GlobTool.Name, 'FindFiles', 'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.', {
properties: {
pattern: {
description: "The glob pattern to match against (e.g., '**/*.py', 'docs/*.md').",
type: 'string',
},
path: {
description: 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.',
type: 'string',
},
case_sensitive: {
description: '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',
});
this.rootDirectory = rootDirectory;
this.config = config;
this.rootDirectory = path.resolve(rootDirectory);
}
/**
* Checks if a given path is within the root directory bounds.
* This security check prevents accessing files outside the designated root directory.
*
* @param pathToCheck The absolute path to validate
* @returns True if the path is within the root directory, false otherwise
*/
isWithinRoot(pathToCheck) {
const absolutePathToCheck = path.resolve(pathToCheck);
const normalizedPath = path.normalize(absolutePathToCheck);
const normalizedRoot = path.normalize(this.rootDirectory);
const rootWithSep = normalizedRoot.endsWith(path.sep)
? normalizedRoot
: normalizedRoot + path.sep;
return (normalizedPath === normalizedRoot ||
normalizedPath.startsWith(rootWithSep));
}
/**
* Validates the parameters for the tool.
*/
validateToolParams(params) {
if (this.schema.parameters &&
!SchemaValidator.validate(this.schema.parameters, params)) {
return "Parameters failed schema validation. Ensure 'pattern' is a string, 'path' (if provided) is a string, and 'case_sensitive' (if provided) is a boolean.";
}
const searchDirAbsolute = path.resolve(this.rootDirectory, params.path || '.');
if (!this.isWithinRoot(searchDirAbsolute)) {
return `Search path ("${searchDirAbsolute}") resolves outside the tool's root directory ("${this.rootDirectory}").`;
}
const targetDir = searchDirAbsolute || this.rootDirectory;
try {
if (!fs.existsSync(targetDir)) {
return `Search path does not exist ${targetDir}`;
}
if (!fs.statSync(targetDir).isDirectory()) {
return `Search path is not a directory: ${targetDir}`;
}
}
catch (e) {
return `Error accessing search path: ${e}`;
}
if (!params.pattern ||
typeof params.pattern !== 'string' ||
params.pattern.trim() === '') {
return "The 'pattern' parameter cannot be empty.";
}
return null;
}
/**
* Gets a description of the glob operation.
*/
getDescription(params) {
let description = `'${params.pattern}'`;
if (params.path) {
const searchDir = path.resolve(this.rootDirectory, params.path || '.');
const relativePath = makeRelative(searchDir, this.rootDirectory);
description += ` within ${shortenPath(relativePath)}`;
}
return description;
}
/**
* Executes the glob search with the given parameters
*/
async execute(params, signal) {
const validationError = this.validateToolParams(params);
if (validationError) {
return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
returnDisplay: validationError,
};
}
try {
const searchDirAbsolute = path.resolve(this.rootDirectory, params.path || '.');
// Get centralized file discovery service
const respectGitIgnore = params.respect_git_ignore ??
this.config.getFileFilteringRespectGitIgnore();
const fileDiscovery = this.config.getFileService();
const entries = (await glob(params.pattern, {
cwd: searchDirAbsolute,
withFileTypes: true,
nodir: true,
stat: true,
nocase: !params.case_sensitive,
dot: true,
ignore: ['**/node_modules/**', '**/.git/**'],
follow: false,
signal,
}));
// Apply git-aware filtering if enabled and in git repository
let filteredEntries = entries;
let gitIgnoredCount = 0;
if (respectGitIgnore) {
const relativePaths = entries.map((p) => path.relative(this.rootDirectory, p.fullpath()));
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.fullpath()));
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: message,
returnDisplay: `No files found`,
};
}
// Set filtering such that we first show the most recent files
const oneDayInMs = 24 * 60 * 60 * 1000;
const nowTimestamp = new Date().getTime();
// Sort the filtered entries using the new helper function
const sortedEntries = sortFileEntries(filteredEntries, nowTimestamp, oneDayInMs);
const sortedAbsolutePaths = sortedEntries.map((entry) => entry.fullpath());
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: resultMessage,
returnDisplay: `Found ${fileCount} matching file(s)`,
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`GlobLogic execute Error: ${errorMessage}`, error);
return {
llmContent: `Error during glob search operation: ${errorMessage}`,
returnDisplay: `Error: An unexpected error occurred.`,
};
}
}
}
//# sourceMappingURL=glob.js.map