directory_tree outputs JSON
This commit is contained in:
parent
a6dfe1e9b6
commit
188cd7653c
1 changed files with 47 additions and 39 deletions
|
|
@ -34,7 +34,7 @@ function expandHome(filepath: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store allowed directories in normalized form
|
// Store allowed directories in normalized form
|
||||||
const allowedDirectories = args.map(dir =>
|
const allowedDirectories = args.map(dir =>
|
||||||
normalizePath(path.resolve(expandHome(dir)))
|
normalizePath(path.resolve(expandHome(dir)))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ async function validatePath(requestedPath: string): Promise<string> {
|
||||||
const absolute = path.isAbsolute(expandedPath)
|
const absolute = path.isAbsolute(expandedPath)
|
||||||
? path.resolve(expandedPath)
|
? path.resolve(expandedPath)
|
||||||
: path.resolve(process.cwd(), expandedPath);
|
: path.resolve(process.cwd(), expandedPath);
|
||||||
|
|
||||||
const normalizedRequested = normalizePath(absolute);
|
const normalizedRequested = normalizePath(absolute);
|
||||||
|
|
||||||
// Check if path is within allowed directories
|
// Check if path is within allowed directories
|
||||||
|
|
@ -195,7 +195,7 @@ async function searchFiles(
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const fullPath = path.join(currentPath, entry.name);
|
const fullPath = path.join(currentPath, entry.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate each path before processing
|
// Validate each path before processing
|
||||||
await validatePath(fullPath);
|
await validatePath(fullPath);
|
||||||
|
|
@ -227,7 +227,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
|
||||||
// Ensure consistent line endings for diff
|
// Ensure consistent line endings for diff
|
||||||
const normalizedOriginal = normalizeLineEndings(originalContent);
|
const normalizedOriginal = normalizeLineEndings(originalContent);
|
||||||
const normalizedNew = normalizeLineEndings(newContent);
|
const normalizedNew = normalizeLineEndings(newContent);
|
||||||
|
|
||||||
return createTwoFilesPatch(
|
return createTwoFilesPatch(
|
||||||
filepath,
|
filepath,
|
||||||
filepath,
|
filepath,
|
||||||
|
|
@ -245,33 +245,33 @@ async function applyFileEdits(
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Read file content and normalize line endings
|
// Read file content and normalize line endings
|
||||||
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
|
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
|
||||||
|
|
||||||
// Apply edits sequentially
|
// Apply edits sequentially
|
||||||
let modifiedContent = content;
|
let modifiedContent = content;
|
||||||
for (const edit of edits) {
|
for (const edit of edits) {
|
||||||
const normalizedOld = normalizeLineEndings(edit.oldText);
|
const normalizedOld = normalizeLineEndings(edit.oldText);
|
||||||
const normalizedNew = normalizeLineEndings(edit.newText);
|
const normalizedNew = normalizeLineEndings(edit.newText);
|
||||||
|
|
||||||
// If exact match exists, use it
|
// If exact match exists, use it
|
||||||
if (modifiedContent.includes(normalizedOld)) {
|
if (modifiedContent.includes(normalizedOld)) {
|
||||||
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
|
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, try line-by-line matching with flexibility for whitespace
|
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||||
const oldLines = normalizedOld.split('\n');
|
const oldLines = normalizedOld.split('\n');
|
||||||
const contentLines = modifiedContent.split('\n');
|
const contentLines = modifiedContent.split('\n');
|
||||||
let matchFound = false;
|
let matchFound = false;
|
||||||
|
|
||||||
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||||
const potentialMatch = contentLines.slice(i, i + oldLines.length);
|
const potentialMatch = contentLines.slice(i, i + oldLines.length);
|
||||||
|
|
||||||
// Compare lines with normalized whitespace
|
// Compare lines with normalized whitespace
|
||||||
const isMatch = oldLines.every((oldLine, j) => {
|
const isMatch = oldLines.every((oldLine, j) => {
|
||||||
const contentLine = potentialMatch[j];
|
const contentLine = potentialMatch[j];
|
||||||
return oldLine.trim() === contentLine.trim();
|
return oldLine.trim() === contentLine.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
// Preserve original indentation of first line
|
// Preserve original indentation of first line
|
||||||
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
||||||
|
|
@ -286,33 +286,33 @@ async function applyFileEdits(
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
|
|
||||||
contentLines.splice(i, oldLines.length, ...newLines);
|
contentLines.splice(i, oldLines.length, ...newLines);
|
||||||
modifiedContent = contentLines.join('\n');
|
modifiedContent = contentLines.join('\n');
|
||||||
matchFound = true;
|
matchFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matchFound) {
|
if (!matchFound) {
|
||||||
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
|
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unified diff
|
// Create unified diff
|
||||||
const diff = createUnifiedDiff(content, modifiedContent, filePath);
|
const diff = createUnifiedDiff(content, modifiedContent, filePath);
|
||||||
|
|
||||||
// Format diff with appropriate number of backticks
|
// Format diff with appropriate number of backticks
|
||||||
let numBackticks = 3;
|
let numBackticks = 3;
|
||||||
while (diff.includes('`'.repeat(numBackticks))) {
|
while (diff.includes('`'.repeat(numBackticks))) {
|
||||||
numBackticks++;
|
numBackticks++;
|
||||||
}
|
}
|
||||||
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
|
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
await fs.writeFile(filePath, modifiedContent, 'utf-8');
|
await fs.writeFile(filePath, modifiedContent, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedDiff;
|
return formattedDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,11 +376,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
{
|
{
|
||||||
name: "directory_tree",
|
name: "directory_tree",
|
||||||
description:
|
description:
|
||||||
"Get a recursive tree view of files and directories starting from a specified path. " +
|
"Get a recursive tree view of files and directories as a JSON structure. " +
|
||||||
"Results are formatted in a hierarchical ASCII tree structure with proper indentation " +
|
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||||
"using pipes and dashes (│ ├ └ ─). Files and directories are distinguished " +
|
"Files have no children array, while directories always have a children array (which may be empty). " +
|
||||||
"with [F] and [D] prefixes. This tool provides a comprehensive visualization of nested " +
|
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
||||||
"directory structures. Only works within allowed directories.",
|
|
||||||
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -413,7 +412,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list_allowed_directories",
|
name: "list_allowed_directories",
|
||||||
description:
|
description:
|
||||||
"Returns the list of directories that this server is allowed to access. " +
|
"Returns the list of directories that this server is allowed to access. " +
|
||||||
"Use this to understand which directories are available before trying to access files.",
|
"Use this to understand which directories are available before trying to access files.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
|
|
@ -518,36 +517,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "directory_tree": {
|
case "directory_tree": {
|
||||||
const parsed = ListDirectoryArgsSchema.safeParse(args);
|
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildTree(currentPath: string, prefix = ""): Promise<string> {
|
interface TreeEntry {
|
||||||
|
name: string;
|
||||||
|
type: 'file' | 'directory';
|
||||||
|
children?: TreeEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildTree(currentPath: string): Promise<TreeEntry[]> {
|
||||||
const validPath = await validatePath(currentPath);
|
const validPath = await validatePath(currentPath);
|
||||||
const entries = await fs.readdir(validPath, {withFileTypes: true});
|
const entries = await fs.readdir(validPath, {withFileTypes: true});
|
||||||
let result = "";
|
const result: TreeEntry[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < entries.length; i++) {
|
for (const entry of entries) {
|
||||||
const entry = entries[i];
|
const entryData: TreeEntry = {
|
||||||
const isLast = i === entries.length - 1;
|
name: entry.name,
|
||||||
const connector = isLast ? "└── " : "├── ";
|
type: entry.isDirectory() ? 'directory' : 'file'
|
||||||
const newPrefix = prefix + (isLast ? " " : "│ ");
|
};
|
||||||
|
|
||||||
result += `${prefix}${connector}${entry.isDirectory() ? "[D]" : "[F]"} ${entry.name}\n`;
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const subPath = path.join(currentPath, entry.name);
|
const subPath = path.join(currentPath, entry.name);
|
||||||
result += await buildTree(subPath, newPrefix);
|
entryData.children = await buildTree(subPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.push(entryData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const treeOutput = await buildTree(parsed.data.path);
|
const treeData = await buildTree(parsed.data.path);
|
||||||
return {
|
return {
|
||||||
content: [{type: "text", text: treeOutput}],
|
content: [{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(treeData, null, 2)
|
||||||
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -592,9 +600,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
|
||||||
case "list_allowed_directories": {
|
case "list_allowed_directories": {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -622,4 +630,4 @@ async function runServer() {
|
||||||
runServer().catch((error) => {
|
runServer().catch((error) => {
|
||||||
console.error("Fatal error running server:", error);
|
console.error("Fatal error running server:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue