mirror of https://github.com/buster-so/buster.git
ls tool
This commit is contained in:
parent
58c2855a72
commit
6544a9cfb0
|
@ -6,7 +6,7 @@ import z from 'zod';
|
|||
import { DEFAULT_ANTHROPIC_OPTIONS } from '../../llm/providers/gateway';
|
||||
import { Sonnet4 } from '../../llm/sonnet-4';
|
||||
import { createIdleTool } from '../../tools';
|
||||
import { createEditFileTool, createMultiEditFileTool, createWriteFileTool } from '../../tools/file-tools';
|
||||
import { createEditFileTool, createLsTool, createMultiEditFileTool, createWriteFileTool } from '../../tools/file-tools';
|
||||
import { createBashTool } from '../../tools/file-tools/bash-tool/bash-tool';
|
||||
import { createGrepTool } from '../../tools/file-tools/grep-tool/grep-tool';
|
||||
import { createReadFileTool } from '../../tools/file-tools/read-file-tool/read-file-tool';
|
||||
|
@ -80,6 +80,10 @@ export function createDocsAgent(docsAgentOptions: DocsAgentOptions) {
|
|||
messageId: docsAgentOptions.messageId,
|
||||
projectDirectory: docsAgentOptions.folder_structure,
|
||||
});
|
||||
const lsTool = createLsTool({
|
||||
messageId: docsAgentOptions.messageId,
|
||||
projectDirectory: docsAgentOptions.folder_structure,
|
||||
});
|
||||
|
||||
// Create planning tools with simple context
|
||||
async function stream({ messages }: DocsStreamOptions) {
|
||||
|
@ -106,6 +110,7 @@ export function createDocsAgent(docsAgentOptions: DocsAgentOptions) {
|
|||
bashTool,
|
||||
editFileTool,
|
||||
multiEditFileTool,
|
||||
lsTool,
|
||||
},
|
||||
messages: [systemMessage, ...messages],
|
||||
stopWhen: STOP_CONDITIONS,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export { createBashTool } from './bash-tool/bash-tool';
|
||||
export { createListFilesTool } from './list-files-tool/list-files-tool';
|
||||
export { createReadFileTool as createReadFilesTool } from './read-file-tool/read-file-tool';
|
||||
export { createLsTool } from './ls-tool/ls-tool';
|
||||
export { createReadFileTool } from './read-file-tool/read-file-tool';
|
||||
export { createWriteFileTool } from './write-file-tool/write-file-tool';
|
||||
export { createEditFileTool } from './edit-file-tool/edit-file-tool';
|
||||
export { createMultiEditFileTool } from './multi-edit-file-tool/multi-edit-file-tool';
|
||||
export { createGrepTool as createGrepSearchTool } from './grep-tool/grep-tool';
|
||||
export { createGrepTool } from './grep-tool/grep-tool';
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
import { wrapTraced } from 'braintrust';
|
||||
import type {
|
||||
ListFilesToolContext,
|
||||
ListFilesToolInput,
|
||||
ListFilesToolOutput,
|
||||
} from './list-files-tool';
|
||||
|
||||
export function createListFilesToolExecute(context: ListFilesToolContext) {
|
||||
return wrapTraced(
|
||||
async (input: ListFilesToolInput): Promise<ListFilesToolOutput> => {
|
||||
const { paths, options } = input;
|
||||
|
||||
if (!paths || paths.length === 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const sandbox = context.sandbox;
|
||||
|
||||
if (!sandbox) {
|
||||
return {
|
||||
results: paths.map((targetPath) => ({
|
||||
status: 'error' as const,
|
||||
path: targetPath,
|
||||
error_message: 'tree command requires sandbox environment',
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const targetPath of paths) {
|
||||
try {
|
||||
const flags = ['--gitignore'];
|
||||
|
||||
if (options?.depth) {
|
||||
flags.push('-L', options.depth.toString());
|
||||
}
|
||||
if (options?.all) {
|
||||
flags.push('-a');
|
||||
}
|
||||
if (options?.dirsOnly) {
|
||||
flags.push('-d');
|
||||
}
|
||||
if (options?.followSymlinks) {
|
||||
flags.push('-l');
|
||||
}
|
||||
if (options?.ignorePattern) {
|
||||
flags.push('-I', options.ignorePattern);
|
||||
}
|
||||
|
||||
const command = `tree ${flags.join(' ')} "${targetPath}"`;
|
||||
const result = await sandbox.process.executeCommand(command);
|
||||
|
||||
if (result.exitCode === 0) {
|
||||
const pwdResult = await sandbox.process.executeCommand('pwd');
|
||||
|
||||
results.push({
|
||||
status: 'success' as const,
|
||||
path: targetPath,
|
||||
output: result.result,
|
||||
currentDirectory: pwdResult.result.trim(),
|
||||
});
|
||||
} else {
|
||||
results.push({
|
||||
status: 'error' as const,
|
||||
path: targetPath,
|
||||
error_message: result.result || `Command failed with exit code ${result.exitCode}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
status: 'error' as const,
|
||||
path: targetPath,
|
||||
error_message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { results };
|
||||
} catch (error) {
|
||||
const errorMessage = `Execution error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
const output = {
|
||||
results: paths.map((targetPath) => ({
|
||||
status: 'error' as const,
|
||||
path: targetPath,
|
||||
error_message: errorMessage,
|
||||
})),
|
||||
};
|
||||
|
||||
return output;
|
||||
}
|
||||
},
|
||||
{ name: 'list-files-tool-execute' }
|
||||
);
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
import type { Sandbox } from '@buster/sandbox';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createListFilesToolExecute } from './list-files-tool-execute';
|
||||
|
||||
const ListFilesOptionsSchema = z.object({
|
||||
depth: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Limit directory depth with -L flag (e.g., 2 for two levels deep)'),
|
||||
all: z.boolean().optional().describe('Use -a flag to include hidden files and directories'),
|
||||
dirsOnly: z.boolean().optional().describe('Use -d flag to show only directories'),
|
||||
ignorePattern: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Use -I flag with pattern to exclude files/dirs (e.g., "node_modules|*.log")'),
|
||||
followSymlinks: z.boolean().optional().describe('Use -l flag to follow symbolic links'),
|
||||
});
|
||||
|
||||
export const ListFilesToolInputSchema = z.object({
|
||||
paths: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'Array of paths to display tree structure for. Can be absolute paths (e.g., /path/to/directory) or relative paths (e.g., ./relative/path).'
|
||||
),
|
||||
options: ListFilesOptionsSchema.optional().describe('Options for tree command execution'),
|
||||
});
|
||||
|
||||
const ListFilesToolOutputSchema = z.object({
|
||||
results: z.array(
|
||||
z.discriminatedUnion('status', [
|
||||
z.object({
|
||||
status: z.literal('success'),
|
||||
path: z.string(),
|
||||
output: z.string().describe('Raw output from tree command'),
|
||||
currentDirectory: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The current working directory when the tree was generated'),
|
||||
}),
|
||||
z.object({
|
||||
status: z.literal('error'),
|
||||
path: z.string(),
|
||||
error_message: z.string(),
|
||||
}),
|
||||
])
|
||||
),
|
||||
});
|
||||
|
||||
const ListFilesToolContextSchema = z.object({
|
||||
messageId: z.string().describe('The message ID for database updates'),
|
||||
sandbox: z
|
||||
.custom<Sandbox>(
|
||||
(val) => {
|
||||
return val && typeof val === 'object' && 'id' in val && 'process' in val;
|
||||
},
|
||||
{ message: 'Invalid Sandbox instance' }
|
||||
)
|
||||
.describe('Sandbox instance for file operations'),
|
||||
});
|
||||
|
||||
export type ListFilesToolInput = z.infer<typeof ListFilesToolInputSchema>;
|
||||
export type ListFilesToolOutput = z.infer<typeof ListFilesToolOutputSchema>;
|
||||
export type ListFilesToolContext = z.infer<typeof ListFilesToolContextSchema>;
|
||||
|
||||
export function createListFilesTool<
|
||||
TAgentContext extends ListFilesToolContext = ListFilesToolContext,
|
||||
>(context: TAgentContext) {
|
||||
const execute = createListFilesToolExecute(context);
|
||||
|
||||
return tool({
|
||||
description: `Displays the directory structure in a hierarchical tree format. Automatically excludes git-ignored files. Supports various options like depth limiting, showing only directories, following symlinks, and custom ignore patterns. Returns the raw text output showing the file system hierarchy with visual tree branches that clearly show parent-child relationships. Accepts both absolute and relative paths and can handle bulk operations through an array of paths.`,
|
||||
inputSchema: ListFilesToolInputSchema,
|
||||
outputSchema: ListFilesToolOutputSchema,
|
||||
execute,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
import { readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { LsToolContext, LsToolInput, LsToolOutput } from './ls-tool';
|
||||
|
||||
export const IGNORE_PATTERNS = [
|
||||
'node_modules/',
|
||||
'__pycache__/',
|
||||
'.git/',
|
||||
'dist/',
|
||||
'build/',
|
||||
'target/',
|
||||
'vendor/',
|
||||
'bin/',
|
||||
'obj/',
|
||||
'.idea/',
|
||||
'.vscode/',
|
||||
'.zig-cache/',
|
||||
'zig-out',
|
||||
'.coverage',
|
||||
'coverage/',
|
||||
'vendor/',
|
||||
'tmp/',
|
||||
'temp/',
|
||||
'.cache/',
|
||||
'cache/',
|
||||
'logs/',
|
||||
'.venv/',
|
||||
'venv/',
|
||||
'env/',
|
||||
];
|
||||
|
||||
const LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Check if a path matches any of the ignore patterns
|
||||
*/
|
||||
function shouldIgnore(
|
||||
relativePath: string,
|
||||
ignorePatterns: string[]
|
||||
): boolean {
|
||||
const normalizedPath = relativePath.replace(/\\/g, '/');
|
||||
|
||||
for (const pattern of ignorePatterns) {
|
||||
// Remove leading '!' if present (for consistency with glob patterns)
|
||||
const cleanPattern = pattern.startsWith('!') ? pattern.slice(1) : pattern;
|
||||
|
||||
// Simple glob matching
|
||||
if (cleanPattern.endsWith('*')) {
|
||||
const prefix = cleanPattern.slice(0, -1);
|
||||
if (normalizedPath.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
} else if (cleanPattern.endsWith('/')) {
|
||||
// Directory pattern
|
||||
const dirName = cleanPattern.slice(0, -1);
|
||||
if (
|
||||
normalizedPath === dirName ||
|
||||
normalizedPath.startsWith(dirName + '/')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Exact match
|
||||
if (normalizedPath === cleanPattern) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively list files in a directory
|
||||
*/
|
||||
async function listFilesRecursive(
|
||||
dirPath: string,
|
||||
basePath: string,
|
||||
ignorePatterns: string[],
|
||||
files: string[],
|
||||
limit: number
|
||||
): Promise<void> {
|
||||
if (files.length >= limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (files.length >= limit) {
|
||||
break;
|
||||
}
|
||||
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
const relativePath = path.relative(basePath, fullPath);
|
||||
|
||||
// Check if should ignore
|
||||
if (shouldIgnore(relativePath, ignorePatterns)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Recurse into directory
|
||||
await listFilesRecursive(
|
||||
fullPath,
|
||||
basePath,
|
||||
ignorePatterns,
|
||||
files,
|
||||
limit
|
||||
);
|
||||
} else if (entry.isFile()) {
|
||||
files.push(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently skip directories we can't read
|
||||
console.warn(`Could not read directory ${dirPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a directory tree structure
|
||||
*/
|
||||
function renderDir(
|
||||
dirPath: string,
|
||||
depth: number,
|
||||
dirs: Set<string>,
|
||||
filesByDir: Map<string, string[]>
|
||||
): string {
|
||||
const indent = ' '.repeat(depth);
|
||||
let output = '';
|
||||
|
||||
if (depth > 0) {
|
||||
output += `${indent}${path.basename(dirPath)}/\n`;
|
||||
}
|
||||
|
||||
const childIndent = ' '.repeat(depth + 1);
|
||||
const children = Array.from(dirs)
|
||||
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
||||
.sort();
|
||||
|
||||
// Render subdirectories first
|
||||
for (const child of children) {
|
||||
output += renderDir(child, depth + 1, dirs, filesByDir);
|
||||
}
|
||||
|
||||
// Render files
|
||||
const files = filesByDir.get(dirPath) || [];
|
||||
for (const file of files.sort()) {
|
||||
output += `${childIndent}${file}\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the execute function for the ls tool
|
||||
*/
|
||||
export function createLsToolExecute(context: LsToolContext) {
|
||||
return async function execute(input: LsToolInput): Promise<LsToolOutput> {
|
||||
const { messageId, projectDirectory } = context;
|
||||
const searchPath = path.resolve(
|
||||
projectDirectory,
|
||||
input.path || projectDirectory
|
||||
);
|
||||
|
||||
console.info(`Listing directory ${searchPath} for message ${messageId}`);
|
||||
|
||||
try {
|
||||
// Validate the path exists and is a directory
|
||||
const stats = await stat(searchPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return {
|
||||
success: false,
|
||||
path: searchPath,
|
||||
output: '',
|
||||
count: 0,
|
||||
truncated: false,
|
||||
errorMessage: `Path is not a directory: ${searchPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Build ignore patterns
|
||||
const ignorePatterns = [
|
||||
...IGNORE_PATTERNS,
|
||||
...(input.ignore || []),
|
||||
];
|
||||
|
||||
// List files
|
||||
const files: string[] = [];
|
||||
await listFilesRecursive(
|
||||
searchPath,
|
||||
searchPath,
|
||||
ignorePatterns,
|
||||
files,
|
||||
LIMIT
|
||||
);
|
||||
|
||||
// Build directory structure
|
||||
const dirs = new Set<string>();
|
||||
const filesByDir = new Map<string, string[]>();
|
||||
|
||||
for (const file of files) {
|
||||
const dir = path.dirname(file);
|
||||
const parts = dir === '.' ? [] : dir.split(path.sep);
|
||||
|
||||
// Add all parent directories
|
||||
for (let i = 0; i <= parts.length; i++) {
|
||||
const dirPath = i === 0 ? '.' : parts.slice(0, i).join('/');
|
||||
dirs.add(dirPath);
|
||||
}
|
||||
|
||||
// Add file to its directory
|
||||
if (!filesByDir.has(dir)) {
|
||||
filesByDir.set(dir, []);
|
||||
}
|
||||
filesByDir.get(dir)?.push(path.basename(file));
|
||||
}
|
||||
|
||||
// Render directory tree
|
||||
const output = `${searchPath}/\n` + renderDir('.', 0, dirs, filesByDir);
|
||||
|
||||
console.info(
|
||||
`Listed ${files.length} file(s) in ${searchPath}${files.length >= LIMIT ? ' (truncated)' : ''}`
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: searchPath,
|
||||
output,
|
||||
count: files.length,
|
||||
truncated: files.length >= LIMIT,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`Error listing directory ${searchPath}:`, errorMessage);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
path: searchPath,
|
||||
output: '',
|
||||
count: 0,
|
||||
truncated: false,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { tool } from 'ai';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { createLsToolExecute } from './ls-tool-execute';
|
||||
|
||||
// Read description from file
|
||||
const DESCRIPTION = readFileSync(path.join(__dirname, 'ls.txt'), 'utf-8');
|
||||
|
||||
export const LsToolInputSchema = z.object({
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'The absolute path to the directory to list (must be absolute, not relative). Defaults to project root.'
|
||||
),
|
||||
ignore: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('List of glob patterns to ignore'),
|
||||
});
|
||||
|
||||
export const LsToolOutputSchema = z.object({
|
||||
success: z.boolean().describe('Whether the listing was successful'),
|
||||
path: z.string().describe('The path that was listed'),
|
||||
output: z.string().describe('Tree-formatted directory listing'),
|
||||
count: z.number().describe('Number of files listed'),
|
||||
truncated: z.boolean().describe('Whether the listing was truncated at limit'),
|
||||
errorMessage: z.string().optional().describe('Error message if failed'),
|
||||
});
|
||||
|
||||
export const LsToolContextSchema = z.object({
|
||||
messageId: z.string().describe('The message ID for database updates'),
|
||||
projectDirectory: z.string().describe('The root directory of the project'),
|
||||
});
|
||||
|
||||
export type LsToolInput = z.infer<typeof LsToolInputSchema>;
|
||||
export type LsToolOutput = z.infer<typeof LsToolOutputSchema>;
|
||||
export type LsToolContext = z.infer<typeof LsToolContextSchema>;
|
||||
|
||||
/**
|
||||
* Factory function to create the ls tool
|
||||
*/
|
||||
export function createLsTool<
|
||||
TAgentContext extends LsToolContext = LsToolContext,
|
||||
>(context: TAgentContext) {
|
||||
const execute = createLsToolExecute(context);
|
||||
|
||||
return tool({
|
||||
description: DESCRIPTION,
|
||||
inputSchema: LsToolInputSchema,
|
||||
outputSchema: LsToolOutputSchema,
|
||||
execute,
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.
|
|
@ -12,13 +12,13 @@ export { createModifyReportsTool } from './visualization-tools/reports/modify-re
|
|||
export { createExecuteSqlTool } from './database-tools/execute-sql/execute-sql';
|
||||
export { executeSqlDocsAgent } from './database-tools/super-execute-sql/super-execute-sql';
|
||||
// File tools - factory functions
|
||||
export { createListFilesTool } from './file-tools/list-files-tool/list-files-tool';
|
||||
export { createReadFileTool as createReadFilesTool } from './file-tools/read-file-tool/read-file-tool';
|
||||
export { createLsTool } from './file-tools/ls-tool/ls-tool';
|
||||
export { createReadFileTool } from './file-tools/read-file-tool/read-file-tool';
|
||||
export { createWriteFileTool } from './file-tools/write-file-tool/write-file-tool';
|
||||
export { createEditFileTool } from './file-tools/edit-file-tool/edit-file-tool';
|
||||
export { createMultiEditFileTool } from './file-tools/multi-edit-file-tool/multi-edit-file-tool';
|
||||
export { createBashTool } from './file-tools/bash-tool/bash-tool';
|
||||
export { createGrepTool as createGrepSearchTool } from './file-tools/grep-tool/grep-tool';
|
||||
export { createGrepTool } from './file-tools/grep-tool/grep-tool';
|
||||
// Web tools - factory functions
|
||||
export { createWebSearchTool } from './web-tools/web-search-tool';
|
||||
|
||||
|
|
Loading…
Reference in New Issue