Skip to main content
Glama
vaultManager.ts5.79 kB
import fs from 'fs/promises'; import path from 'path'; import { logger } from './logger.js'; export interface VaultInfo { path: string; name: string; noteCount?: number; size?: number; lastModified?: Date; } export class VaultManager { private vaultCache: Map<string, VaultInfo> = new Map(); // Common vault locations on Windows private getDefaultVaultPaths(): string[] { const userProfile = process.env.USERPROFILE || ''; return [ path.join(userProfile, 'Documents', 'Obsidian'), path.join(userProfile, 'OneDrive', 'Documents', 'Obsidian'), path.join(userProfile, 'Documents'), path.join(userProfile, 'OneDrive', 'Documents'), ]; } // Discover vaults from Obsidian's configuration async discoverVaults(): Promise<VaultInfo[]> { const vaults: VaultInfo[] = []; // Try to read Obsidian's vault registry const appData = process.env.APPDATA || ''; const obsidianConfigPath = path.join(appData, 'obsidian', 'obsidian.json'); try { const configData = await fs.readFile(obsidianConfigPath, 'utf-8'); const config = JSON.parse(configData); if (config.vaults) { for (const vaultId in config.vaults) { const vault = config.vaults[vaultId]; if (vault.path && await this.isValidVault(vault.path)) { vaults.push({ path: vault.path, name: path.basename(vault.path), lastModified: vault.ts ? new Date(vault.ts) : undefined, }); } } } } catch (error) { logger.debug('Could not read Obsidian config:', error); } // Also check default locations const defaultPaths = this.getDefaultVaultPaths(); for (const basePath of defaultPaths) { try { const entries = await fs.readdir(basePath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const vaultPath = path.join(basePath, entry.name); if (await this.isValidVault(vaultPath)) { // Don't add duplicates if (!vaults.find(v => v.path === vaultPath)) { vaults.push({ path: vaultPath, name: entry.name, }); } } } } } catch (error) { logger.debug(`Could not scan directory ${basePath}:`, error); } } // Check environment variable if (process.env.OBSIDIAN_VAULT) { const envVault = process.env.OBSIDIAN_VAULT; if (await this.isValidVault(envVault)) { if (!vaults.find(v => v.path === envVault)) { vaults.push({ path: envVault, name: path.basename(envVault), }); } } } return vaults; } // Check if a directory is a valid Obsidian vault async isValidVault(vaultPath: string): Promise<boolean> { try { const stats = await fs.stat(vaultPath); if (!stats.isDirectory()) return false; // Check for .obsidian folder or any .md files const entries = await fs.readdir(vaultPath); const hasObsidianFolder = entries.includes('.obsidian'); const hasMdFiles = entries.some(e => e.endsWith('.md')); return hasObsidianFolder || hasMdFiles; } catch { return false; } } // Validate vault path and ensure it exists async validateVault(vaultPath: string): Promise<void> { if (!await this.isValidVault(vaultPath)) { throw new Error(`Invalid vault path: ${vaultPath}`); } } // Get full file path, handling .md extension getFilePath(vaultPath: string, notePath: string): string { // Normalize the note path let normalizedPath = notePath.replace(/\\/g, '/'); // Add .md extension if not present if (!normalizedPath.endsWith('.md')) { normalizedPath += '.md'; } // Ensure safe path (no directory traversal) const safePath = path.normalize(normalizedPath).replace(/^(\.\.(\/|\\|$))+/, ''); return path.join(vaultPath, safePath); } // List all markdown files in a vault async listMarkdownFiles(vaultPath: string, folderPath?: string): Promise<string[]> { const basePath = folderPath ? path.join(vaultPath, folderPath) : vaultPath; const files: string[] = []; async function scanDir(dir: string) { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.')) { await scanDir(fullPath); } else if (entry.isFile() && entry.name.endsWith('.md')) { // Return path relative to vault const relativePath = path.relative(vaultPath, fullPath); files.push(relativePath.replace(/\\/g, '/')); } } } await scanDir(basePath); return files; } // Get vault statistics async getVaultStats(vaultPath: string): Promise<VaultInfo> { await this.validateVault(vaultPath); const files = await this.listMarkdownFiles(vaultPath); let totalSize = 0; let lastModified = new Date(0); for (const file of files) { try { const filePath = path.join(vaultPath, file); const stats = await fs.stat(filePath); totalSize += stats.size; if (stats.mtime > lastModified) { lastModified = stats.mtime; } } catch (error) { logger.debug(`Could not stat file ${file}:`, error); } } return { path: vaultPath, name: path.basename(vaultPath), noteCount: files.length, size: totalSize, lastModified, }; } }

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

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