/**
* analyze_dependencies tool implementation
*
* Analyzes dependency graph with KG enrichment
*/
import { readFile, readdir } from 'fs/promises';
import { join, extname, relative, resolve, dirname } from 'path';
import { existsSync } from 'fs';
import { getDatabase } from '../knowledge-graph/database.js';
import { getYAGOResolver } from '../knowledge-graph/yago-resolver.js';
interface AnalyzeDependenciesArgs {
path: string;
include_external?: boolean;
include_internal?: boolean;
max_depth?: number;
}
interface Dependency {
name: string;
version?: string;
type: 'external' | 'internal';
dependents: string[];
dependencies: string[];
depth: number;
kg_enrichment?: {
description?: string;
yago_uri?: string;
facts_count?: number;
};
}
interface DependencyGraph {
nodes: Map<string, Dependency>;
edges: Array<[string, string]>;
circular: string[][];
orphans: string[];
stats: {
total_external: number;
total_internal: number;
max_depth: number;
circular_count: number;
};
}
export async function analyzeDependencies(
args: AnalyzeDependenciesArgs
): Promise<{ content: Array<{ type: string; text: string }> }> {
const {
path,
include_external = true,
include_internal = true,
max_depth = 5,
} = args;
const graph: DependencyGraph = {
nodes: new Map(),
edges: [],
circular: [],
orphans: [],
stats: {
total_external: 0,
total_internal: 0,
max_depth: 0,
circular_count: 0,
},
};
// Analyze external dependencies (from package.json)
if (include_external) {
await analyzeExternalDependencies(path, graph);
}
// Analyze internal dependencies (import/require statements)
if (include_internal) {
await analyzeInternalDependencies(path, graph, max_depth);
}
// Detect circular dependencies
detectCircularDependencies(graph);
// Enrich with knowledge graph
await enrichDependenciesWithKG(graph);
// Store in database as RDF triples
await storeDependencyGraph(graph);
// Format results
const results = formatDependencyResults(graph);
return {
content: [
{
type: 'text',
text: results,
},
],
};
}
/**
* Analyze external dependencies from package.json
*/
async function analyzeExternalDependencies(
path: string,
graph: DependencyGraph
): Promise<void> {
const pkgPath = join(path, 'package.json');
if (!existsSync(pkgPath)) {
return;
}
const pkgContent = await readFile(pkgPath, 'utf-8');
const pkg = JSON.parse(pkgContent);
// Process dependencies
const deps = {
...pkg.dependencies,
...pkg.devDependencies,
...pkg.peerDependencies,
};
for (const [name, version] of Object.entries(deps)) {
if (!graph.nodes.has(name)) {
graph.nodes.set(name, {
name,
version: version as string,
type: 'external',
dependents: [],
dependencies: [],
depth: 1,
});
graph.stats.total_external++;
}
}
}
/**
* Analyze internal module dependencies
*/
async function analyzeInternalDependencies(
path: string,
graph: DependencyGraph,
maxDepth: number
): Promise<void> {
const files = await getCodeFiles(path);
const moduleDeps = new Map<string, Set<string>>();
for (const file of files) {
const content = await readFile(file, 'utf-8');
const imports = extractImports(content);
const relPath = relative(path, file);
if (!moduleDeps.has(relPath)) {
moduleDeps.set(relPath, new Set());
}
for (const imp of imports) {
// Skip external dependencies
if (!imp.startsWith('.') && !imp.startsWith('/')) {
continue;
}
// Resolve relative import
const resolved = resolveImport(file, imp, path);
if (resolved) {
moduleDeps.get(relPath)!.add(resolved);
}
}
}
// Build dependency graph
for (const [module, deps] of moduleDeps.entries()) {
if (!graph.nodes.has(module)) {
graph.nodes.set(module, {
name: module,
type: 'internal',
dependents: [],
dependencies: [],
depth: 0,
});
graph.stats.total_internal++;
}
const node = graph.nodes.get(module)!;
for (const dep of deps) {
node.dependencies.push(dep);
graph.edges.push([module, dep]);
if (!graph.nodes.has(dep)) {
graph.nodes.set(dep, {
name: dep,
type: 'internal',
dependents: [],
dependencies: [],
depth: 0,
});
graph.stats.total_internal++;
}
graph.nodes.get(dep)!.dependents.push(module);
}
}
// Calculate depths
calculateDepths(graph, maxDepth);
}
/**
* Get all code files in directory
*/
async function getCodeFiles(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
continue;
}
if (entry.isDirectory()) {
const subFiles = await getCodeFiles(fullPath);
files.push(...subFiles);
} else if (entry.isFile()) {
const ext = extname(entry.name);
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
files.push(fullPath);
}
}
}
return files;
}
/**
* Extract import statements from code
*/
function extractImports(content: string): string[] {
const imports: string[] = [];
// ES6 imports: import ... from 'module'
const es6Imports = content.matchAll(/import\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g);
for (const match of es6Imports) {
imports.push(match[1]);
}
// CommonJS requires: require('module')
const cjsRequires = content.matchAll(/require\(['"]([^'"]+)['"]\)/g);
for (const match of cjsRequires) {
imports.push(match[1]);
}
// Dynamic imports: import('module')
const dynamicImports = content.matchAll(/import\(['"]([^'"]+)['"]\)/g);
for (const match of dynamicImports) {
imports.push(match[1]);
}
return imports;
}
/**
* Resolve relative import path
*/
function resolveImport(fromFile: string, importPath: string, rootPath: string): string | null {
try {
const fromDir = dirname(fromFile);
const resolved = resolve(fromDir, importPath);
// Try common extensions
const extensions = ['.ts', '.js', '.tsx', '.jsx', '.json'];
for (const ext of extensions) {
const withExt = resolved + ext;
if (existsSync(withExt)) {
return relative(rootPath, withExt);
}
}
// Try index files
for (const ext of extensions) {
const indexFile = join(resolved, `index${ext}`);
if (existsSync(indexFile)) {
return relative(rootPath, indexFile);
}
}
return null;
} catch {
return null;
}
}
/**
* Calculate dependency depths using BFS
*/
function calculateDepths(graph: DependencyGraph, maxDepth: number): void {
// Find root nodes (no dependents)
const roots: string[] = [];
for (const [name, node] of graph.nodes.entries()) {
if (node.type === 'internal' && node.dependents.length === 0) {
roots.push(name);
node.depth = 0;
}
}
// BFS to calculate depths
const queue = [...roots];
const visited = new Set<string>();
while (queue.length > 0) {
const current = queue.shift()!;
if (visited.has(current)) continue;
visited.add(current);
const node = graph.nodes.get(current)!;
if (node.depth >= maxDepth) continue;
for (const dep of node.dependencies) {
const depNode = graph.nodes.get(dep);
if (depNode && depNode.type === 'internal') {
const newDepth = node.depth + 1;
if (newDepth < depNode.depth || depNode.depth === 0) {
depNode.depth = newDepth;
graph.stats.max_depth = Math.max(graph.stats.max_depth, newDepth);
}
queue.push(dep);
}
}
}
// Find orphans (no dependencies and no dependents, internal only)
for (const [name, node] of graph.nodes.entries()) {
if (
node.type === 'internal' &&
node.dependencies.length === 0 &&
node.dependents.length === 0
) {
graph.orphans.push(name);
}
}
}
/**
* Detect circular dependencies using DFS
*/
function detectCircularDependencies(graph: DependencyGraph): void {
const visited = new Set<string>();
const recursionStack = new Set<string>();
const cycles: string[][] = [];
function dfs(node: string, path: string[]): void {
visited.add(node);
recursionStack.add(node);
path.push(node);
const deps = graph.nodes.get(node)?.dependencies || [];
for (const dep of deps) {
if (!graph.nodes.has(dep)) continue;
if (!visited.has(dep)) {
dfs(dep, [...path]);
} else if (recursionStack.has(dep)) {
// Found cycle
const cycleStart = path.indexOf(dep);
const cycle = path.slice(cycleStart).concat(dep);
cycles.push(cycle);
}
}
recursionStack.delete(node);
}
for (const node of graph.nodes.keys()) {
if (!visited.has(node)) {
dfs(node, []);
}
}
graph.circular = cycles;
graph.stats.circular_count = cycles.length;
}
/**
* Enrich dependencies with knowledge graph data
*/
async function enrichDependenciesWithKG(graph: DependencyGraph): Promise<void> {
const yagoResolver = getYAGOResolver();
const externals = Array.from(graph.nodes.values()).filter((n) => n.type === 'external');
// Enrich top external dependencies
for (const dep of externals.slice(0, 20)) {
try {
const entities = await yagoResolver.resolveEntity(dep.name, 1);
if (entities.length > 0) {
const entity = entities[0];
dep.kg_enrichment = {
description: entity.description,
yago_uri: entity.uri,
facts_count: entity.facts.length,
};
}
} catch (error) {
console.error(`Failed to enrich ${dep.name}:`, error);
}
}
}
/**
* Store dependency graph as RDF triples
*/
async function storeDependencyGraph(graph: DependencyGraph): Promise<void> {
const db = await getDatabase();
for (const [source, target] of graph.edges) {
try {
await db.insertRDFTriple({
subject: `module:${source}`,
predicate: 'dependsOn',
object: `module:${target}`,
graph: 'dependencies',
});
} catch (error) {
// Ignore duplicate entries
}
}
}
/**
* Format dependency results for display
*/
function formatDependencyResults(graph: DependencyGraph): string {
const lines: string[] = [];
lines.push('# Dependency Analysis');
lines.push('');
// Statistics
lines.push('## Statistics');
lines.push(`- **Total External:** ${graph.stats.total_external}`);
lines.push(`- **Total Internal:** ${graph.stats.total_internal}`);
lines.push(`- **Max Depth:** ${graph.stats.max_depth}`);
lines.push(`- **Circular Dependencies:** ${graph.stats.circular_count}`);
lines.push(`- **Orphan Modules:** ${graph.orphans.length}`);
lines.push('');
// External dependencies with KG enrichment
if (graph.stats.total_external > 0) {
lines.push('## External Dependencies');
const externals = Array.from(graph.nodes.values())
.filter((n) => n.type === 'external')
.sort((a, b) => b.dependents.length - a.dependents.length);
for (const dep of externals.slice(0, 20)) {
lines.push(`### ${dep.name}`);
lines.push(`- **Version:** ${dep.version || 'N/A'}`);
if (dep.kg_enrichment) {
if (dep.kg_enrichment.description) {
lines.push(`- **Description:** ${dep.kg_enrichment.description}`);
}
if (dep.kg_enrichment.facts_count) {
lines.push(`- **Knowledge Graph:** ${dep.kg_enrichment.facts_count} facts in YAGO`);
}
}
lines.push('');
}
}
// Circular dependencies
if (graph.circular.length > 0) {
lines.push('## Circular Dependencies');
lines.push('');
lines.push('⚠️ **Warning:** Circular dependencies detected!');
lines.push('');
for (let i = 0; i < graph.circular.length; i++) {
const cycle = graph.circular[i];
lines.push(`${i + 1}. ${cycle.join(' → ')}`);
}
lines.push('');
}
// Orphan modules
if (graph.orphans.length > 0) {
lines.push('## Orphan Modules');
lines.push('');
lines.push('These modules have no dependencies or dependents:');
lines.push('');
for (const orphan of graph.orphans) {
lines.push(`- ${orphan}`);
}
lines.push('');
}
// Top dependencies by usage
const byUsage = Array.from(graph.nodes.values())
.filter((n) => n.type === 'internal')
.sort((a, b) => b.dependents.length - a.dependents.length)
.slice(0, 10);
if (byUsage.length > 0) {
lines.push('## Most Depended Upon (Internal)');
lines.push('');
for (const dep of byUsage) {
lines.push(`- **${dep.name}** (${dep.dependents.length} dependents)`);
}
lines.push('');
}
return lines.join('\n');
}