/**
* 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;
}
}