brain-mcp
Version:
Brain MCP Server - Semantic knowledge base access for Claude Code via Model Context Protocol. Provides intelligent search and navigation of files from multiple locations through native MCP tools.
305 lines • 11.9 kB
JavaScript
;
/**
* Graph construction from parsed notes
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphBuilder = void 0;
const fs = __importStar(require("fs"));
const fast_glob_1 = __importDefault(require("fast-glob"));
const graphlib_1 = require("graphlib");
const ParserFactory_1 = require("../parser/ParserFactory");
const LinkResolver_1 = require("../parser/LinkResolver");
const ChunkingService_1 = require("../parser/ChunkingService");
class GraphBuilder {
notesRoot;
parserFactory;
linkResolver;
supportedPatterns;
constructor(notesRoot) {
this.notesRoot = notesRoot;
this.parserFactory = new ParserFactory_1.ParserFactory();
this.linkResolver = new LinkResolver_1.LinkResolver(notesRoot);
this.supportedPatterns = this.parserFactory.getSupportedPatterns();
}
async buildGraph(filePaths) {
// Initialize link resolver
await this.linkResolver.initialize();
if (!filePaths) {
filePaths = await (0, fast_glob_1.default)(this.supportedPatterns, {
cwd: this.notesRoot,
absolute: true,
ignore: ['**/node_modules/**', '**/.*/**']
});
}
// Parse all notes
const nodes = new Map();
const allLinks = [];
for (const filePath of filePaths) {
try {
const parser = this.parserFactory.getParser(filePath);
if (!parser) {
console.warn(`Warning: No parser available for ${filePath}`);
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const note = await parser.parse(filePath, content, this.notesRoot);
// Generate semantic chunks for the note
note.chunks = ChunkingService_1.ChunkingService.createChunks(content, note.title, note.headings, note.path);
nodes.set(note.path, {
note,
incomingLinks: [],
inDegree: 0,
outDegree: 0,
clusterId: null,
centralityScore: 0
});
// Resolve links
for (const link of note.outgoingLinks) {
const resolvedLink = await this.linkResolver.resolveLink(link);
allLinks.push(resolvedLink);
}
}
catch (error) {
console.warn(`Warning: Failed to parse ${filePath}: ${error}`);
continue;
}
}
// Process links and build connections
const brokenLinks = [];
for (const link of allLinks) {
if (link.isBroken || !link.targetPath || !nodes.has(link.targetPath)) {
brokenLinks.push(link);
continue;
}
// Add to target node's incoming links
const targetNode = nodes.get(link.targetPath);
targetNode.incomingLinks.push(link);
}
// Calculate degrees
for (const node of nodes.values()) {
node.inDegree = node.incomingLinks.length;
node.outDegree = node.note.outgoingLinks.length;
}
// Build graph for algorithms
const graph = this.buildGraphLibGraph(nodes, allLinks);
// Detect clusters
const clusters = this.detectClusters(graph, nodes);
// Assign cluster IDs to nodes
clusters.forEach((cluster, clusterId) => {
for (const nodePath of cluster) {
const node = nodes.get(nodePath);
if (node) {
node.clusterId = clusterId;
}
}
});
// Calculate centrality scores
this.calculateCentrality(graph, nodes);
// Identify hub nodes and orphans
const hubNodes = this.findHubNodes(nodes);
const orphanNodes = this.findOrphanNodes(nodes);
return {
nodes,
clusters,
hubNodes,
orphanNodes,
brokenLinks,
lastUpdated: new Date()
};
}
buildGraphLibGraph(nodes, links) {
const graph = new graphlib_1.Graph({ directed: true });
// Add nodes
for (const path of nodes.keys()) {
graph.setNode(path);
}
// Add edges
for (const link of links) {
if (!link.isBroken && link.targetPath && nodes.has(link.targetPath)) {
graph.setEdge(link.sourcePath, link.targetPath);
}
}
return graph;
}
detectClusters(graph, nodes) {
// Simple connected components detection
// For more advanced clustering, could implement Louvain algorithm
const visited = new Set();
const clusters = [];
// Convert to undirected for component detection
const undirected = new graphlib_1.Graph({ directed: false });
// Copy nodes
for (const node of graph.nodes()) {
undirected.setNode(node);
}
// Copy edges as undirected
for (const edge of graph.edges()) {
undirected.setEdge(edge.v, edge.w);
}
for (const node of undirected.nodes()) {
if (!visited.has(node)) {
const component = new Set();
this.dfsComponent(undirected, node, visited, component);
// Only include clusters with more than one node
if (component.size > 1) {
clusters.push(component);
}
}
}
return clusters;
}
dfsComponent(graph, node, visited, component) {
visited.add(node);
component.add(node);
const neighbors = graph.neighbors(node) || [];
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
this.dfsComponent(graph, neighbor, visited, component);
}
}
}
calculateCentrality(graph, nodes) {
if (graph.nodeCount() === 0) {
return;
}
// Simple degree centrality calculation
// For more advanced metrics, could implement PageRank, betweenness centrality
const maxDegree = Math.max(...Array.from(nodes.values()).map(n => n.inDegree + n.outDegree));
for (const [path, node] of nodes.entries()) {
const totalDegree = node.inDegree + node.outDegree;
node.centralityScore = maxDegree > 0 ? totalDegree / maxDegree : 0;
}
}
findHubNodes(nodes, topN = 10) {
// Sort by combination of degree and centrality
const sortedNodes = Array.from(nodes.entries())
.sort(([, a], [, b]) => {
const scoreA = a.inDegree + a.outDegree + a.centralityScore;
const scoreB = b.inDegree + b.outDegree + b.centralityScore;
return scoreB - scoreA;
});
const hubs = [];
for (const [path, node] of sortedNodes.slice(0, topN)) {
// Only include nodes with significant connections
if (node.inDegree + node.outDegree >= 2) {
hubs.push(path);
}
}
return hubs;
}
findOrphanNodes(nodes) {
const orphans = [];
for (const [path, node] of nodes.entries()) {
if (node.inDegree === 0 && node.outDegree === 0) {
orphans.push(path);
}
}
return orphans;
}
async updateGraph(graph, changedFiles, removedFiles = []) {
// Remove deleted files
for (const filePath of removedFiles) {
graph.nodes.delete(filePath);
}
// Update link resolver index
if (removedFiles.length > 0) {
await this.linkResolver.updateIndex([], removedFiles);
}
// Parse changed files
const newLinks = [];
for (const filePath of changedFiles) {
try {
const parser = this.parserFactory.getParser(filePath);
if (!parser) {
console.warn(`Warning: No parser available for ${filePath}`);
continue;
}
const content = fs.readFileSync(filePath, 'utf-8');
const note = await parser.parse(filePath, content, this.notesRoot);
// Generate semantic chunks for the note
note.chunks = ChunkingService_1.ChunkingService.createChunks(content, note.title, note.headings, note.path);
const newNode = {
note,
incomingLinks: [],
inDegree: 0,
outDegree: 0,
clusterId: null,
centralityScore: 0
};
graph.nodes.set(note.path, newNode);
// Resolve links
for (const link of note.outgoingLinks) {
const resolvedLink = await this.linkResolver.resolveLink(link);
newLinks.push(resolvedLink);
}
}
catch (error) {
console.warn(`Warning: Failed to parse ${filePath}: ${error}`);
continue;
}
}
// Update link resolver index
if (changedFiles.length > 0) {
await this.linkResolver.updateIndex(changedFiles, []);
}
// Recalculate affected connections
// Clear all incoming links and recalculate
for (const node of graph.nodes.values()) {
node.incomingLinks = [];
}
// Rebuild all incoming links
for (const node of graph.nodes.values()) {
for (const link of node.note.outgoingLinks) {
if (!link.isBroken && link.targetPath && graph.nodes.has(link.targetPath)) {
const targetNode = graph.nodes.get(link.targetPath);
targetNode.incomingLinks.push(link);
}
}
}
// Recalculate degrees
for (const node of graph.nodes.values()) {
node.inDegree = node.incomingLinks.length;
node.outDegree = node.note.outgoingLinks.length;
}
graph.lastUpdated = new Date();
return graph;
}
}
exports.GraphBuilder = GraphBuilder;
//# sourceMappingURL=GraphBuilder.js.map