import { neo4jAdapter } from '../../adapters/Neo4jAdapter.js'; import { ingestRepository } from '../../services/GraphIngestor.js'; import { sentinelEngine } from '../../services/SentinelEngine.js'; // Handlers async function handleQueryKnowledgeGraph(args: any) { const { query, type = 'search', limit = 20 } = args; try { let results: any[]; let cypherUsed: string; switch (type) { case 'cypher': if (query.toLowerCase().includes('delete') || query.toLowerCase().includes('drop') || query.toLowerCase().includes('remove')) { throw new Error('Destructive queries not allowed via this interface'); } cypherUsed = query; results = await neo4jAdapter.executeQuery(query); break; case 'labels': cypherUsed = 'CALL db.labels() YIELD label RETURN label'; results = await neo4jAdapter.readQuery(cypherUsed); break; case 'relationships': cypherUsed = 'CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType'; results = await neo4jAdapter.readQuery(cypherUsed); break; case 'search': default: cypherUsed = ` MATCH (n) WHERE n.name CONTAINS $query OR n.title CONTAINS $query OR n.content CONTAINS $query OR n.description CONTAINS $query RETURN n, labels(n) as labels LIMIT $limit `; results = await neo4jAdapter.readQuery(cypherUsed, { query, limit: parseInt(String(limit)) }); break; } let gapDetection: any = null; if (type === 'search' && results.length < 3) { const confidence = results.length === 0 ? 0.0 : results.length / 10; gapDetection = await sentinelEngine.detectAndRegisterGap({ query, source: 'graph_query', resultCount: results.length, confidence, metadata: { queryType: type, limit } }); } return { content: [{ type: 'text', text: JSON.stringify({ queryType: type, query: query, cypherExecuted: cypherUsed, resultCount: results.length, results: results, ...(gapDetection?.detected && { autoGapDetected: { gapId: gapDetection.gapId, reason: gapDetection.reason, message: '🔍 Knowledge gap auto-registered by Sentinel' } }) }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, queryType: type, query: query, hint: 'Check Neo4j connection or query syntax' }, null, 2) }], isError: true }; } } async function handleCreateGraphNode(args: any) { const { label, properties } = args; if (!label || !properties) { throw new Error('Label and properties are required'); } if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(label)) { throw new Error('Invalid label format'); } try { const result = await neo4jAdapter.createNode(label, { ...properties, createdAt: new Date().toISOString(), source: 'neural-bridge' }); return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'node_created', label: label, node: result }, null, 2) }] }; } catch (error: any) { throw new Error(`Failed to create node: ${error.message}`); } } async function handleCreateGraphRelationship(args: any) { const { fromNodeId, toNodeId, relationshipType, properties = {} } = args; if (!fromNodeId || !toNodeId || !relationshipType) { throw new Error('fromNodeId, toNodeId, and relationshipType are required'); } if (!/^[A-Z_][A-Z0-9_]*$/.test(relationshipType)) { throw new Error('Invalid relationship type format (use UPPERCASE_WITH_UNDERSCORES)'); } try { const result = await neo4jAdapter.createRelationship( fromNodeId, toNodeId, relationshipType, { ...properties, createdAt: new Date().toISOString(), source: 'neural-bridge' } ); return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'relationship_created', type: relationshipType, result: result }, null, 2) }] }; } catch (error: any) { throw new Error(`Failed to create relationship: ${error.message}`); } } async function handleGetNodeConnections(args: any) { const { nodeId, direction = 'both', limit = 50 } = args; if (!nodeId) { throw new Error('nodeId is required'); } try { const connections = await neo4jAdapter.getNodeRelationships( nodeId, direction, limit ); return { content: [{ type: 'text', text: JSON.stringify({ nodeId: nodeId, direction: direction, connectionCount: connections.length, connections: connections }, null, 2) }] }; } catch (error: any) { throw new Error(`Failed to get connections: ${error.message}`); } } async function handleGetHarvestStats(args: any) { const timeRange = args?.timeRange || '24h'; try { const stats = await neo4jAdapter.readQuery(` MATCH (n) WITH labels(n) as nodeLabels, count(*) as cnt UNWIND nodeLabels as label RETURN label, sum(cnt) as count ORDER BY count DESC `); const relStats = await neo4jAdapter.readQuery(` MATCH ()-[r]->() RETURN type(r) as type, count(r) as count ORDER BY count DESC `); return { content: [{ type: 'text', text: JSON.stringify({ timeRange, nodesByLabel: stats, relationshipsByType: relStats, lastUpdated: new Date().toISOString() }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: JSON.stringify({ timeRange, filesScanned: 288, linesOfCode: 58317, nodesCreated: 1247, relationshipsCreated: 3891, note: 'Simulated stats - Neo4j connection issue', error: error.message }, null, 2) }] }; } } async function handleGetGraphStats(args: any) { try { const health = await neo4jAdapter.healthCheck(); const labelCounts = await neo4jAdapter.readQuery(` CALL db.labels() YIELD label CALL { WITH label MATCH (n) WHERE label IN labels(n) RETURN count(n) as count } RETURN label, count ORDER BY count DESC `).catch(() => []); const relCounts = await neo4jAdapter.readQuery(` CALL db.relationshipTypes() YIELD relationshipType CALL { WITH relationshipType MATCH ()-[r]->() WHERE type(r) = relationshipType RETURN count(r) as count } RETURN relationshipType, count ORDER BY count DESC `).catch(() => []); return { content: [{ type: 'text', text: JSON.stringify({ health: health, labels: labelCounts, relationshipTypes: relCounts, timestamp: new Date().toISOString() }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, hint: 'Neo4j may not be running. Start with: docker-compose up neo4j' }, null, 2) }], isError: true }; } } async function handleIngestKnowledgeGraph(args: any) { const { path: targetPath, name, maxDepth = 10 } = args; if (!targetPath) { throw new Error('Path is required'); } try { console.error(`[Neural Bridge] 🚀 Starting ingestion of: ${targetPath}`); const result = await ingestRepository({ rootPath: targetPath, repositoryName: name, maxDepth: maxDepth }); return { content: [{ type: 'text', text: JSON.stringify({ success: result.success, repositoryId: result.repositoryId, stats: result.stats, errors: result.errors, message: result.success ? `Successfully ingested ${result.stats.totalNodes} nodes with ${result.stats.relationshipsCreated} relationships` : 'Ingestion failed - check errors' }, null, 2) }] }; } catch (error: any) { return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, hint: 'Ensure Neo4j is running and path exists' }, null, 2) }], isError: true }; } } export const GRAPH_TOOLS = [ { name: 'query_knowledge_graph', description: 'Query the Neo4j knowledge graph with Cypher or natural language search', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Cypher query or search term' }, type: { type: 'string', enum: ['search', 'cypher', 'labels', 'relationships'], description: 'Query type: search (text), cypher (raw), labels (list all), relationships (list types)' }, limit: { type: 'number', description: 'Maximum results to return (default: 20)' } }, required: ['query'] }, handler: handleQueryKnowledgeGraph }, { name: 'create_graph_node', description: 'Create or merge a node in the knowledge graph', inputSchema: { type: 'object', properties: { label: { type: 'string', description: 'Node label (e.g., Component, Document, Concept)' }, properties: { type: 'object', description: 'Node properties (name, description, etc.)' } }, required: ['label', 'properties'] }, handler: handleCreateGraphNode }, { name: 'create_graph_relationship', description: 'Create a relationship between two nodes in the graph', inputSchema: { type: 'object', properties: { fromNodeId: { type: 'string', description: 'Source node ID' }, toNodeId: { type: 'string', description: 'Target node ID' }, relationshipType: { type: 'string', description: 'Relationship type (e.g., DEPENDS_ON, CONTAINS, RELATED_TO)' }, properties: { type: 'object', description: 'Optional relationship properties' } }, required: ['fromNodeId', 'toNodeId', 'relationshipType'] }, handler: handleCreateGraphRelationship }, { name: 'get_node_connections', description: 'Get all connections (relationships) for a specific node', inputSchema: { type: 'object', properties: { nodeId: { type: 'string', description: 'Node ID to get connections for' }, direction: { type: 'string', enum: ['in', 'out', 'both'], description: 'Direction of relationships' }, limit: { type: 'number', description: 'Maximum connections to return' } }, required: ['nodeId'] }, handler: handleGetNodeConnections }, { name: 'get_harvest_stats', description: 'Get OmniHarvester statistics and recent activity', inputSchema: { type: 'object', properties: { timeRange: { type: 'string', enum: ['1h', '24h', '7d', '30d'], description: 'Time range for statistics' } } }, handler: handleGetHarvestStats }, { name: 'get_graph_stats', description: 'Get knowledge graph statistics (node counts, relationship counts by type)', inputSchema: { type: 'object', properties: {} }, handler: handleGetGraphStats }, { name: 'ingest_knowledge_graph', description: 'Ingest a repository or directory into the knowledge graph. Creates Repository, Directory, and File nodes with CONTAINS relationships.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Path to repository or directory to ingest' }, name: { type: 'string', description: 'Optional name for the repository' }, maxDepth: { type: 'number', description: 'Maximum directory depth to traverse (default: 10)' } }, required: ['path'] }, handler: handleIngestKnowledgeGraph } ];