@agentforce/adk
Version:
AgentForce Agent Development Kit - A powerful framework for building AI agents and servers
240 lines (212 loc) • 8.49 kB
text/typescript
import { readdirSync, statSync } from "fs";
import { join, relative } from "path";
import type { ToolImplementation } from "../types";
import { parseGitignore, shouldExclude } from "../utils/gitignore_parser";
interface TreeNode {
name: string;
path: string;
isDirectory: boolean;
children?: TreeNode[];
isIgnored?: boolean;
}
/**
* Create ASCII tree structure from directory
*/
function buildTree(dirPath: string, rootPath: string, excludes: string[], maxDepth: number, currentDepth = 0): TreeNode[] {
if (currentDepth >= maxDepth) return [];
const nodes: TreeNode[] = [];
try {
const items = readdirSync(dirPath);
const sortedItems = items.sort((a, b) => {
// Directories first, then files, both alphabetically
const aPath = join(dirPath, a);
const bPath = join(dirPath, b);
const aIsDir = statSync(aPath).isDirectory();
const bIsDir = statSync(bPath).isDirectory();
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
return a.localeCompare(b);
});
for (const item of sortedItems) {
const fullPath = join(dirPath, item);
const relativePath = relative(rootPath, fullPath);
try {
const stats = statSync(fullPath);
const isDirectory = stats.isDirectory();
const isExcluded = shouldExclude(relativePath, excludes, !isDirectory);
const node: TreeNode = {
name: item,
path: relativePath,
isDirectory,
isIgnored: isExcluded,
};
if (isDirectory) {
if (isExcluded) {
// If directory is ignored, show only the folder itself, not its children
node.children = [];
} else {
// Recursively get children for non-ignored directories
node.children = buildTree(fullPath, rootPath, excludes, maxDepth, currentDepth + 1);
}
}
nodes.push(node);
} catch {
// Skip files/directories we can't access
continue;
}
}
} catch {
// Skip directories we can't read
}
return nodes;
}
/**
* Convert tree structure to ASCII representation
*/
function treeToAscii(nodes: TreeNode[], prefix = "", _isLast = true): string {
let result = "";
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const isLastItem = i === nodes.length - 1;
const connector = isLastItem ? "└── " : "├── ";
const childPrefix = prefix + (isLastItem ? " " : "│ ");
// Add folder indicators and ignored markers
let displayName = node.name;
if (node.isDirectory) {
displayName += "/";
}
if (node.isIgnored) {
displayName += " (ignored)";
}
result += prefix + connector + displayName + "\n";
// Add children if it's a directory and not ignored
if (node.isDirectory && node.children && node.children.length > 0 && !node.isIgnored) {
result += treeToAscii(node.children, childPrefix, false);
}
}
return result;
}
/**
* Markdown ASCII tree creator tool
* Creates a visual ASCII tree representation of a directory structure
*/
export const md_create_ascii_tree: ToolImplementation = {
definition: {
type: "function",
function: {
name: "md_create_ascii_tree",
description: "Create an ASCII tree view of a directory structure, respecting .gitignore patterns. Ignored directories are shown but their contents are not explored.",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "The directory path to create tree for (defaults to current working directory)",
},
use_gitignore: {
type: "boolean",
description: "Whether to use .gitignore patterns for exclusions (default: true)",
},
additional_excludes: {
type: "array",
description: "Additional directories/files to exclude from tree",
},
max_depth: {
type: "number",
description: "Maximum depth to traverse (default: 5)",
},
include_files: {
type: "boolean",
description: "Whether to include files in the tree (default: true)",
},
markdown_format: {
type: "boolean",
description: "Whether to format output as markdown code block (default: true)",
},
},
required: [],
},
},
},
execute: async (args: Record<string, any>) => {
const searchPath = args.path || process.cwd();
const useGitignore = args.use_gitignore !== false; // Default true
const additionalExcludes = args.additional_excludes || [];
const maxDepth = args.max_depth || 5;
const includeFiles = args.include_files !== false; // Default true
const markdownFormat = args.markdown_format !== false; // Default true
try {
// Get excludes from .gitignore if enabled
let excludes: string[] = [...additionalExcludes];
if (useGitignore) {
const gitignoreExcludes = parseGitignore(searchPath);
excludes = [...excludes, ...gitignoreExcludes];
}
// Build the tree structure
const tree = buildTree(searchPath, searchPath, excludes, maxDepth);
// Filter out files if not requested
const filterTree = (nodes: TreeNode[]): TreeNode[] => {
return nodes.filter(node => {
if (!includeFiles && !node.isDirectory) {
return false;
}
if (node.children) {
node.children = filterTree(node.children);
}
return true;
});
};
const filteredTree = filterTree(tree);
// Convert to ASCII representation
const rootName = searchPath === process.cwd() ? "." : searchPath.split(/[\/\\]/).pop() || searchPath;
let asciiTree = `${rootName}/\n`;
asciiTree += treeToAscii(filteredTree);
// Format as markdown if requested
let output = asciiTree;
if (markdownFormat) {
output = "```\n" + asciiTree + "```";
}
// Calculate statistics
const stats = {
totalDirectories: 0,
totalFiles: 0,
ignoredDirectories: 0,
};
const countNodes = (nodes: TreeNode[]): void => {
for (const node of nodes) {
if (node.isDirectory) {
stats.totalDirectories++;
if (node.isIgnored) {
stats.ignoredDirectories++;
}
if (node.children) {
countNodes(node.children);
}
} else {
stats.totalFiles++;
}
}
};
countNodes(tree);
return {
success: true,
path: searchPath,
tree: output,
statistics: stats,
excludes: excludes,
settings: {
maxDepth,
includeFiles,
useGitignore,
markdownFormat,
},
};
} catch (error: any) {
return {
success: false,
error: error.message,
path: searchPath,
};
}
},
};