Skip to main content
Glama
danielsimonjr

Enhanced Knowledge Graph Memory Server

HierarchyManager.ts7.57 kB
/** * Hierarchy Manager * * Manages entity parent-child relationships and hierarchical operations. * * @module features/HierarchyManager */ import type { Entity, KnowledgeGraph } from '../types/index.js'; import type { GraphStorage } from '../core/GraphStorage.js'; import { EntityNotFoundError, CycleDetectedError } from '../utils/errors.js'; /** * Manages hierarchical entity relationships. */ export class HierarchyManager { constructor(private storage: GraphStorage) {} /** * Set the parent of an entity. * * Validates: * - Entity and parent exist * - Setting parent won't create a cycle * * @param entityName - Entity to set parent for * @param parentName - Parent entity name (null to remove parent) * @returns Updated entity * @throws {EntityNotFoundError} If entity or parent not found * @throws {CycleDetectedError} If setting parent would create a cycle */ async setEntityParent(entityName: string, parentName: string | null): Promise<Entity> { const graph = await this.storage.loadGraph(); const entity = graph.entities.find(e => e.name === entityName); if (!entity) { throw new EntityNotFoundError(entityName); } // If setting a parent, validate it exists and doesn't create a cycle if (parentName !== null) { const parent = graph.entities.find(e => e.name === parentName); if (!parent) { throw new EntityNotFoundError(parentName); } // Check for cycles if (this.wouldCreateCycle(graph, entityName, parentName)) { throw new CycleDetectedError(entityName, parentName); } } entity.parentId = parentName || undefined; entity.lastModified = new Date().toISOString(); await this.storage.saveGraph(graph); return entity; } /** * Check if setting a parent would create a cycle in the hierarchy. * * @param graph - Knowledge graph * @param entityName - Entity to set parent for * @param parentName - Proposed parent * @returns True if cycle would be created */ private wouldCreateCycle( graph: KnowledgeGraph, entityName: string, parentName: string ): boolean { const visited = new Set<string>(); let current: string | undefined = parentName; while (current) { if (visited.has(current)) { return true; // Cycle detected in existing hierarchy } if (current === entityName) { return true; // Would create a cycle } visited.add(current); const currentEntity = graph.entities.find(e => e.name === current); current = currentEntity?.parentId; } return false; } /** * Get the immediate children of an entity. * * @param entityName - Parent entity name * @returns Array of child entities * @throws {EntityNotFoundError} If entity not found */ async getChildren(entityName: string): Promise<Entity[]> { const graph = await this.storage.loadGraph(); // Verify entity exists if (!graph.entities.find(e => e.name === entityName)) { throw new EntityNotFoundError(entityName); } return graph.entities.filter(e => e.parentId === entityName); } /** * Get the parent of an entity. * * @param entityName - Entity name * @returns Parent entity or null if no parent * @throws {EntityNotFoundError} If entity not found */ async getParent(entityName: string): Promise<Entity | null> { const graph = await this.storage.loadGraph(); const entity = graph.entities.find(e => e.name === entityName); if (!entity) { throw new EntityNotFoundError(entityName); } if (!entity.parentId) { return null; } const parent = graph.entities.find(e => e.name === entity.parentId); return parent || null; } /** * Get all ancestors of an entity (parent, grandparent, etc.). * * @param entityName - Entity name * @returns Array of ancestor entities (ordered from immediate parent to root) * @throws {EntityNotFoundError} If entity not found */ async getAncestors(entityName: string): Promise<Entity[]> { const graph = await this.storage.loadGraph(); const ancestors: Entity[] = []; let current = graph.entities.find(e => e.name === entityName); if (!current) { throw new EntityNotFoundError(entityName); } while (current.parentId) { const parent = graph.entities.find(e => e.name === current!.parentId); if (!parent) break; ancestors.push(parent); current = parent; } return ancestors; } /** * Get all descendants of an entity (children, grandchildren, etc.). * * Uses breadth-first traversal. * * @param entityName - Entity name * @returns Array of descendant entities * @throws {EntityNotFoundError} If entity not found */ async getDescendants(entityName: string): Promise<Entity[]> { const graph = await this.storage.loadGraph(); // Verify entity exists if (!graph.entities.find(e => e.name === entityName)) { throw new EntityNotFoundError(entityName); } const descendants: Entity[] = []; const toProcess = [entityName]; while (toProcess.length > 0) { const current = toProcess.shift()!; const children = graph.entities.filter(e => e.parentId === current); for (const child of children) { descendants.push(child); toProcess.push(child.name); } } return descendants; } /** * Get the entire subtree rooted at an entity (entity + all descendants). * * Includes relations between entities in the subtree. * * @param entityName - Root entity name * @returns Knowledge graph containing subtree * @throws {EntityNotFoundError} If entity not found */ async getSubtree(entityName: string): Promise<KnowledgeGraph> { const graph = await this.storage.loadGraph(); const entity = graph.entities.find(e => e.name === entityName); if (!entity) { throw new EntityNotFoundError(entityName); } const descendants = await this.getDescendants(entityName); const subtreeEntities = [entity, ...descendants]; const subtreeEntityNames = new Set(subtreeEntities.map(e => e.name)); // Include relations between entities in the subtree const subtreeRelations = graph.relations.filter( r => subtreeEntityNames.has(r.from) && subtreeEntityNames.has(r.to) ); return { entities: subtreeEntities, relations: subtreeRelations, }; } /** * Get root entities (entities with no parent). * * @returns Array of root entities */ async getRootEntities(): Promise<Entity[]> { const graph = await this.storage.loadGraph(); return graph.entities.filter(e => !e.parentId); } /** * Get the depth of an entity in the hierarchy. * * Root entities have depth 0, their children depth 1, etc. * * @param entityName - Entity name * @returns Depth (number of ancestors) * @throws Error if entity not found */ async getEntityDepth(entityName: string): Promise<number> { const ancestors = await this.getAncestors(entityName); return ancestors.length; } /** * Move an entity to a new parent (maintaining its descendants). * * Alias for setEntityParent. * * @param entityName - Entity to move * @param newParentName - New parent name (null to make root) * @returns Updated entity */ async moveEntity(entityName: string, newParentName: string | null): Promise<Entity> { return await this.setEntityParent(entityName, newParentName); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/danielsimonjr/memory-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server