import { FindCommandBuilder } from '../commands/FindCommandBuilder.js';
import { safeExec } from '../utils/exec.js';
import { getToolHints } from './hints.js';
import { applyPagination, generatePaginationHints, serializeForPagination, createPaginationInfo, type PaginationMetadata } from '../utils/pagination.js';
import { validateToolPath, createErrorResult, checkLargeOutputSafety } from '../utils/toolHelpers.js';
import type { FindFilesQuery, FindFilesResult, FoundFile } from '../types.js';
import fs from 'fs';
export async function findFiles(
query: FindFilesQuery
): Promise<FindFilesResult> {
try {
const validation = validateToolPath(query, 'LOCAL_FIND_FILES');
if (!validation.isValid) {
return validation.errorResult as FindFilesResult;
}
const builder = new FindCommandBuilder();
const { command, args } = builder.fromQuery(query).build();
const result = await safeExec(command, args);
if (!result.success) {
return {
status: 'error',
error: result.stderr || 'Find command failed',
researchGoal: query.researchGoal,
reasoning: query.reasoning,
hints: getToolHints('LOCAL_FIND_FILES', 'error'),
};
}
let filePaths = result.stdout
.split('\0')
.filter((line) => line.trim())
.map((line) => line.trim());
const maxFiles = query.limit || 1000;
filePaths = filePaths.slice(0, maxFiles);
const files: FoundFile[] = await getFileDetails(filePaths);
files.sort((a, b) => {
if (!a.modified || !b.modified) return 0;
return new Date(b.modified).getTime() - new Date(a.modified).getTime();
});
const filesForOutput: FoundFile[] = query.details ? files : files.map((f) => ({ path: f.path, type: f.type }));
const totalFiles = filesForOutput.length;
const filesPerPage = query.filesPerPage || 20;
const filePageNumber = query.filePageNumber || 1;
const totalPages = Math.ceil(totalFiles / filesPerPage);
const startIdx = (filePageNumber - 1) * filesPerPage;
const endIdx = Math.min(startIdx + filesPerPage, totalFiles);
const paginatedFiles = filesForOutput.slice(startIdx, endIdx);
const safetyCheck = checkLargeOutputSafety(paginatedFiles.length, !!query.charLength, {
threshold: 100,
itemType: 'file',
detailed: query.details,
});
if (safetyCheck.shouldBlock) {
return {
status: 'error',
error: safetyCheck.error!,
totalFiles,
researchGoal: query.researchGoal,
reasoning: query.reasoning,
hints: safetyCheck.hints!,
};
}
let finalFiles = paginatedFiles;
let paginationMetadata: PaginationMetadata | null = null;
if (query.charLength) {
const serialized = serializeForPagination(paginatedFiles, false);
const pagination = applyPagination(serialized, query.charOffset ?? 0, query.charLength);
try {
finalFiles = JSON.parse(pagination.paginatedContent);
paginationMetadata = pagination;
} catch {
finalFiles = paginatedFiles;
paginationMetadata = null;
}
}
const status = totalFiles > 0 ? 'hasResults' : 'empty';
const filePaginationHints = [
`📄 Page ${filePageNumber} of ${totalPages} (showing ${finalFiles.length} of ${totalFiles} files)`,
filePageNumber < totalPages ? `▶️ Next: Use filePageNumber=${filePageNumber + 1}` : '✅ Final page',
'📊 Files sorted by modification time (most recent first)',
];
return {
status,
files: finalFiles,
totalFiles,
pagination: {
currentPage: filePageNumber,
totalPages,
filesPerPage,
totalFiles,
hasMore: filePageNumber < totalPages,
},
...(paginationMetadata && { charPagination: createPaginationInfo(paginationMetadata) }),
researchGoal: query.researchGoal,
reasoning: query.reasoning,
hints: [...filePaginationHints, ...getToolHints('LOCAL_FIND_FILES', status), ...(paginationMetadata ? generatePaginationHints(paginationMetadata, { toolName: 'local_find_files' }) : [])],
};
} catch (error) {
return createErrorResult(error, 'LOCAL_FIND_FILES', query) as FindFilesResult;
}
}
async function getFileDetails(filePaths: string[]): Promise<FoundFile[]> {
const files: FoundFile[] = [];
for (const filePath of filePaths) {
try {
const stats = await fs.promises.lstat(filePath);
let type = 'file';
if (stats.isDirectory()) type = 'directory';
else if (stats.isSymbolicLink()) type = 'symlink';
files.push({
path: filePath,
type,
size: stats.size,
modified: stats.mtime.toISOString(),
permissions: stats.mode.toString(8).slice(-3),
});
} catch {
files.push({
path: filePath,
type: 'file',
});
}
}
return files;
}