Skip to main content
Glama
storage.ts5.12 kB
/** * Service for handling file storage operations */ import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; import type { SessionNote, NoteConfig } from '../types/session.js'; import { formatNoteAsMarkdown, generateFilename } from './noteFormatter.js'; const DEFAULT_NOTES_DIR = path.join(os.homedir(), 'notes'); /** * Expand tilde (~) to home directory */ function expandTilde(filepath: string): string { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } /** * Get the notes directory path * Priority: config parameter > SECOND_BRAIN_NOTES_DIR env var > default ~/notes */ export function getNotesDirectory(config?: NoteConfig): string { // 1. Check config parameter if (config?.notesDirectory) { return expandTilde(config.notesDirectory); } // 2. Check environment variable if (process.env.SECOND_BRAIN_NOTES_DIR) { return expandTilde(process.env.SECOND_BRAIN_NOTES_DIR); } // 3. Default to ~/notes return DEFAULT_NOTES_DIR; } /** * Get the project-specific subdirectory */ function getProjectDirectory(notesDir: string, projectName?: string): string { if (!projectName) { return notesDir; } const projectSlug = projectName .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); return path.join(notesDir, projectSlug); } /** * Ensure a directory exists, creating it if necessary */ async function ensureDirectory(dirPath: string): Promise<void> { try { await fs.access(dirPath); } catch { await fs.mkdir(dirPath, { recursive: true }); } } /** * Save a session note to disk */ export async function saveNote( note: SessionNote, config?: NoteConfig ): Promise<string> { const notesDir = getNotesDirectory(config); const projectDir = getProjectDirectory(notesDir, note.projectName); // Ensure the directory exists await ensureDirectory(projectDir); // Generate filename and full path const filename = generateFilename(note); const filePath = path.join(projectDir, filename); // Format note as markdown const markdown = formatNoteAsMarkdown(note); // Write to file await fs.writeFile(filePath, markdown, 'utf-8'); // Save metadata for faster queries await saveNoteMetadata(filePath, note); return filePath; } /** * Read a note from disk */ export async function readNote(filePath: string): Promise<string> { return await fs.readFile(filePath, 'utf-8'); } /** * List all notes for a project */ export async function listNotes( projectName?: string, config?: NoteConfig ): Promise<string[]> { const notesDir = getNotesDirectory(config); const projectDir = getProjectDirectory(notesDir, projectName); try { const files = await fs.readdir(projectDir); return files .filter(f => f.endsWith('.md')) .map(f => path.join(projectDir, f)) .sort() .reverse(); // Most recent first } catch { return []; } } /** * Recursively get all markdown files from a directory */ async function getAllMarkdownFilesRecursive(dir: string): Promise<string[]> { const files: string[] = []; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const subFiles = await getAllMarkdownFilesRecursive(fullPath); files.push(...subFiles); } else if (entry.isFile() && entry.name.endsWith('.md')) { files.push(fullPath); } } } catch { // Directory doesn't exist or can't be read return []; } return files; } /** * Get all note files across all projects * Accepts either a NoteConfig object or a direct directory path string */ export async function getAllNoteFiles(configOrDir?: NoteConfig | string): Promise<string[]> { // Determine the notes directory const notesDir = typeof configOrDir === 'string' ? configOrDir : getNotesDirectory(configOrDir); // Get all markdown files recursively const allFiles = await getAllMarkdownFilesRecursive(notesDir); // Sort by filename (most recent first, assuming timestamp-based naming) return allFiles.sort().reverse(); } /** * Save note metadata to a JSON cache (for faster queries) */ export async function saveNoteMetadata( filePath: string, note: SessionNote ): Promise<void> { const metadataPath = filePath.replace(/\.md$/, '.meta.json'); const metadata = { summary: note.summary, timestamp: note.timestamp, projectName: note.projectName, topic: note.topic, tags: note.tags, analysis: note.analysis, }; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); } /** * Load note metadata from JSON cache */ export async function loadNoteMetadata(filePath: string): Promise<Partial<SessionNote> | null> { const metadataPath = filePath.replace(/\.md$/, '.meta.json'); try { const content = await fs.readFile(metadataPath, 'utf-8'); return JSON.parse(content); } catch { return null; } }

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/VoCoufi/second-brain-mcp'

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