summaryrefslogtreecommitdiff
path: root/packages/server/src/tools/glob.ts
diff options
context:
space:
mode:
authorEvan Senter <[email protected]>2025-04-19 19:45:42 +0100
committerGitHub <[email protected]>2025-04-19 19:45:42 +0100
commit3fce6cea27d3e6129d6c06e528b62e1b11bf7094 (patch)
tree244b8e9ab94f902d65d4bda8739a6538e377ed17 /packages/server/src/tools/glob.ts
parent0c9e1ef61be7db53e6e73b7208b649cd8cbed6c3 (diff)
Starting to modularize into separate cli / server packages. (#55)
* Starting to move a lot of code into packages/server * More of the massive refactor, builds and runs, some issues though. * Fixing outstanding issue with double messages. * Fixing a minor UI issue. * Fixing the build post-merge. * Running formatting. * Addressing comments.
Diffstat (limited to 'packages/server/src/tools/glob.ts')
-rw-r--r--packages/server/src/tools/glob.ts216
1 files changed, 216 insertions, 0 deletions
diff --git a/packages/server/src/tools/glob.ts b/packages/server/src/tools/glob.ts
new file mode 100644
index 00000000..b9a3143c
--- /dev/null
+++ b/packages/server/src/tools/glob.ts
@@ -0,0 +1,216 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import fs from 'fs';
+import path from 'path';
+import fg from 'fast-glob';
+import { SchemaValidator } from '../utils/schemaValidator.js';
+import { BaseTool, ToolResult } from './tools.js';
+import { shortenPath, makeRelative } from '../utils/paths.js';
+
+/**
+ * Parameters for the GlobTool
+ */
+export interface GlobToolParams {
+ /**
+ * The glob pattern to match files against
+ */
+ pattern: string;
+
+ /**
+ * The directory to search in (optional, defaults to current directory)
+ */
+ path?: string;
+}
+
+/**
+ * Implementation of the Glob tool logic (moved from CLI)
+ */
+export class GlobLogic extends BaseTool<GlobToolParams, ToolResult> {
+ static readonly Name = 'glob'; // Keep static name
+
+ /**
+ * The root directory that this tool is grounded in.
+ */
+ private rootDirectory: string;
+
+ /**
+ * Creates a new instance of the GlobLogic
+ * @param rootDirectory Root directory to ground this tool in.
+ */
+ constructor(rootDirectory: string) {
+ super(
+ GlobLogic.Name,
+ '', // Display name handled by CLI wrapper
+ '', // Description handled by CLI wrapper
+ {
+ properties: {
+ pattern: {
+ description:
+ "The glob pattern to match against (e.g., '*.py', 'src/**/*.js', 'docs/*.md').",
+ type: 'string',
+ },
+ path: {
+ description:
+ 'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.',
+ type: 'string',
+ },
+ },
+ required: ['pattern'],
+ type: 'object',
+ },
+ );
+
+ this.rootDirectory = path.resolve(rootDirectory);
+ }
+
+ /**
+ * Checks if a path is within the root directory.
+ */
+ private isWithinRoot(pathToCheck: string): boolean {
+ 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: GlobToolParams): string | null {
+ if (
+ this.schema.parameters &&
+ !SchemaValidator.validate(
+ this.schema.parameters as Record<string, unknown>,
+ params,
+ )
+ ) {
+ return "Parameters failed schema validation. Ensure 'pattern' is a string and 'path' (if provided) is a string.";
+ }
+
+ 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}").`;
+ }
+
+ try {
+ if (!fs.existsSync(searchDirAbsolute)) {
+ return `Search path does not exist: ${shortenPath(makeRelative(searchDirAbsolute, this.rootDirectory))} (absolute: ${searchDirAbsolute})`;
+ }
+ if (!fs.statSync(searchDirAbsolute).isDirectory()) {
+ return `Search path is not a directory: ${shortenPath(makeRelative(searchDirAbsolute, this.rootDirectory))} (absolute: ${searchDirAbsolute})`;
+ }
+ } catch (e: unknown) {
+ 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: GlobToolParams): string {
+ 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: GlobToolParams): Promise<ToolResult> {
+ const validationError = this.validateToolParams(params);
+ if (validationError) {
+ return {
+ llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
+ returnDisplay: `Error: Failed to execute tool.`,
+ };
+ }
+
+ try {
+ const searchDirAbsolute = path.resolve(
+ this.rootDirectory,
+ params.path || '.',
+ );
+
+ const entries = await fg(params.pattern, {
+ cwd: searchDirAbsolute,
+ absolute: true,
+ onlyFiles: true,
+ stats: true,
+ dot: true,
+ ignore: ['**/node_modules/**', '**/.git/**'],
+ followSymbolicLinks: false,
+ suppressErrors: true,
+ });
+
+ if (!entries || entries.length === 0) {
+ const displayPath = makeRelative(searchDirAbsolute, this.rootDirectory);
+ return {
+ llmContent: `No files found matching pattern "${params.pattern}" within ${displayPath || '.'}.`,
+ returnDisplay: `No files found`,
+ };
+ }
+
+ entries.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 sortedRelativePaths = sortedAbsolutePaths.map((absPath) =>
+ makeRelative(absPath, this.rootDirectory),
+ );
+
+ const fileListDescription = sortedRelativePaths.join('\n');
+ const fileCount = sortedRelativePaths.length;
+ const relativeSearchDir = makeRelative(
+ searchDirAbsolute,
+ this.rootDirectory,
+ );
+ const displayPath = shortenPath(
+ relativeSearchDir === '.' ? 'root directory' : relativeSearchDir,
+ );
+
+ return {
+ llmContent: `Found ${fileCount} file(s) matching "${params.pattern}" within ${displayPath}, sorted by modification time (newest first):\n${fileListDescription}`,
+ 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.`,
+ };
+ }
+ }
+}