Skip to main content
Glama
storage.ts4.63 kB
import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { createReadStream } from 'fs'; import { createInterface } from 'readline'; import { KnowledgeGraph } from './knowledgeGraph.js'; import { createDirectory } from '../utils/fs.js'; /** * Class responsible for persisting the knowledge graph to disk */ export class GraphStorage { private filePath: string; private graph: KnowledgeGraph; private isLoading: boolean = false; private loadPromise: Promise<void> | null = null; /** * Create a new GraphStorage instance * @param filePath - Path to the storage file * @param graph - Knowledge graph to persist */ constructor(filePath: string, graph: KnowledgeGraph) { this.filePath = filePath; this.graph = graph; this.loadPromise = this.load(); } /** * Get the loading promise - useful for waiting until loading is complete */ getLoadingPromise(): Promise<void> { return this.loadPromise || Promise.resolve(); } /** * Load the graph from the file (JSONL only) using streaming for better performance * @returns Promise that resolves when loading is complete */ async load(): Promise<void> { if (this.isLoading) { // No logging return this.loadPromise as Promise<void>; } this.isLoading = true; this.loadPromise = new Promise<void>((resolve) => { try { // Ensure directory exists createDirectory(path.dirname(this.filePath)); if (fs.existsSync(this.filePath)) { // Clear existing data this.graph.entities.clear(); this.graph.relations.clear(); // Create read stream and readline interface const fileStream = createReadStream(this.filePath, { encoding: 'utf8' }); const rl = createInterface({ input: fileStream, crlfDelay: Infinity }); let _lineCount = 0; // Process each line as it's read rl.on('line', (line) => { if (!line.trim()) return; // Skip empty lines try { const obj = JSON.parse(line); if (obj._type === 'entity') { this.graph.addEntity(obj); } else if (obj._type === 'relation') { this.graph.addRelation(obj); } _lineCount++; // No progress logging } catch (_err) { // No warn logging } }); // When file is completely read rl.on('close', () => { // No info logging this.isLoading = false; resolve(); }); // Handle errors fileStream.on('error', (_error) => { console.error(`Error reading graph file: ${_error}`); this.isLoading = false; resolve(); // Resolve anyway to prevent hanging }); } else { // Create an empty file if it doesn't exist fs.writeFileSync(this.filePath, '', 'utf8'); // No info logging this.isLoading = false; resolve(); } } catch (_error) { console.error(`Error loading graph: ${_error}`); this.isLoading = false; resolve(); // Resolve anyway to prevent hanging } }); return this.loadPromise; } /** * Save the graph to the file in JSONL format */ async save(): Promise<void> { try { const lines: string[] = []; for (const entity of this.graph.entities.values()) { lines.push(JSON.stringify({ ...entity, _type: 'entity' })); } for (const relSet of this.graph.relations.values()) { for (const relation of relSet) { lines.push(JSON.stringify({ ...relation, _type: 'relation' })); } } fs.writeFileSync(this.filePath, lines.join('\n') + '\n', 'utf8'); // No info logging } catch (_err) { // Just continue } } /** * Log an operation for debugging * @param operation - The operation being performed * @param details - Details about the operation */ logOperation(_operation: string, _details: unknown): void { // No logging } } // Create a default graph instance const memoryPath = process.env.MEMORY_PATH || path.join(os.homedir(), '.mcp-think-tank/memory.jsonl'); // Export the graph and storage for use in tools export const graph = new KnowledgeGraph(); export const graphStorage = new GraphStorage(memoryPath, graph); // We'll look at the file first to understand its implementation

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/flight505/mcp-think-tank'

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