Skip to main content
Glama
VaultManager.ts5.4 kB
import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import path, { basename, isAbsolute, join } from "node:path"; import matter from "gray-matter"; import { DirectoryWalker } from "./DirectoryWalker.js"; import { Indexer } from "./Indexer.js"; import type { DocumentIndex } from "./processor/types.js"; import { Semaphore } from "./semaphore.js"; export interface EnrichedDocument extends DocumentIndex { content: string; stats?: { wordCount: number; lineCount: number; characterCount: number; contentLength: number; hasContent: boolean; }; backlinks?: { filePath: string; title: string; }[]; } export class VaultManager { private vaultPath: string; private isInitialized: boolean = false; private walker: DirectoryWalker; private indexer: Indexer; private ioSemaphore: Semaphore; constructor(vaultPath: string, maxConcurrentIO: number = 10) { this.vaultPath = vaultPath; this.walker = new DirectoryWalker([".md", ".mdx"]); this.indexer = new Indexer(); this.ioSemaphore = new Semaphore(maxConcurrentIO); } async initialize(): Promise<void> { if (this.isInitialized) { return; } if (!existsSync(this.vaultPath)) { throw new Error(`Vault 경로가 존재하지 않습니다: ${this.vaultPath}`); } const filePaths = await this.walker.walk(this.vaultPath, this.ioSemaphore); await this.indexer.build(filePaths, this.ioSemaphore); this.isInitialized = true; } public async getAllDocuments(): Promise<DocumentIndex[]> { await this.initialize(); return this.indexer.getAllDocuments(); } public async searchDocuments(keyword: string): Promise<DocumentIndex[]> { await this.initialize(); return this.indexer.search(keyword); } public async getDocumentInfo( filename: string, options: { includeStats?: boolean; includeBacklinks?: boolean; maxContentPreview?: number; } = {}, ): Promise<EnrichedDocument | null> { await this.initialize(); const fullPath = this.parseFilenameToFullPath(filename); const index = this.indexer.getDocument(fullPath); if (!index) { return null; } const content = await this.getDocumentContent(fullPath); if (content === null) { return null; } const enrichedDoc: EnrichedDocument = { ...index, content: options.maxContentPreview ? content.substring(0, options.maxContentPreview) : content, }; if (options.includeStats) { this.addStats(enrichedDoc, content); } if (options.includeBacklinks) { this.addBacklinks(enrichedDoc, fullPath); } return enrichedDoc; } public async writeDocument( fullPath: string, frontmatter: Record<string, unknown>, ): Promise<void> { await this.ioSemaphore.acquire(); try { const content = (await this.readDocumentContent(fullPath)) || ""; const newDocument = matter.stringify(content, frontmatter); await writeFile(fullPath, newDocument, "utf8"); } finally { this.ioSemaphore.release(); } await this.refresh(); } public async refresh(): Promise<void> { this.isInitialized = false; await this.initialize(); } public getStats() { return { totalFiles: this.indexer.totalFiles, isInitialized: this.isInitialized, vaultPath: this.vaultPath, }; } private parseFilenameToFullPath(filename: string): string { if (isAbsolute(filename) && existsSync(filename)) { return filename; } const exactPath = path.join(this.vaultPath, filename); if (existsSync(exactPath)) { return exactPath; } const candidates = []; if (/\.mdx?$/.test(filename)) { candidates.push(path.join(this.vaultPath, filename)); } else { candidates.push(path.join(this.vaultPath, `${filename}.md`)); candidates.push(path.join(this.vaultPath, `${filename}.mdx`)); } for (const candidate of candidates) { if (existsSync(candidate)) { return candidate; } } const searchTerms = [ filename.replace(/\.mdx?$/, ""), basename(filename, path.extname(filename)), ]; for (const term of searchTerms) { const searchResults = this.indexer.search(term); const foundDoc = searchResults.find((doc) => doc.filePath.includes(term)); if (foundDoc) { return foundDoc.filePath; } } return ""; } private async readDocumentContent(filePath: string): Promise<string | null> { try { return await readFile(filePath, "utf-8"); } catch (error) { console.error(`파일 내용을 읽는 중 오류 발생: ${filePath}`, error); return null; } } private async getDocumentContent(filePath: string): Promise<string | null> { await this.ioSemaphore.acquire(); try { return await readFile(filePath, "utf-8"); } finally { this.ioSemaphore.release(); } } private addStats(doc: EnrichedDocument, content: string): void { doc.stats = { wordCount: content.split(/\s+/).filter(Boolean).length, lineCount: content.split("\n").length, contentLength: content.length, hasContent: content.trim().length > 0, characterCount: content.length, }; } private addBacklinks(doc: EnrichedDocument, fullPath: string): void { const backlinkPaths = this.indexer.getBacklinks(fullPath); doc.backlinks = backlinkPaths.map((path) => { const docIndex = this.indexer.getDocument(path); return { filePath: path, title: docIndex?.frontmatter?.title || join(path).split(/[\\/]/).pop()?.replace(/\.md$/, "") || "Untitled", }; }); } }

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/sunub/obsidian-mcp-server'

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