Skip to main content
Glama

AI Conversation Logger

by fablefang
fileManager.ts6.54 kB
import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { homedir } from 'os'; import type { ProjectConfig } from '../types/index.js'; import { CONSTANTS, FileOperationError } from '../constants/index.js'; export class FileManager { private readonly homeConfigDir: string; constructor() { this.homeConfigDir = join(homedir(), '.ai-conversation-logger'); } private async detectProjectRoot(startPath: string = process.cwd()): Promise<string | null> { const indicators = ['package.json', '.git', 'Cargo.toml', 'go.mod', 'pom.xml', 'composer.json']; let currentDir = startPath; const root = dirname(currentDir); while (currentDir !== root) { for (const indicator of indicators) { const indicatorPath = join(currentDir, indicator); try { await fs.access(indicatorPath); return currentDir; } catch { // Continue searching } } currentDir = dirname(currentDir); } return null; } private getProjectNameFromPath(projectRoot: string): string { return projectRoot.split('/').pop() || 'unknown-project'; } async ensureDirectoryExists(dirPath: string): Promise<void> { try { await fs.mkdir(dirPath, { recursive: true }); } catch { throw new FileOperationError(`Failed to create directory`, dirPath, 'mkdir'); } } async writeFile(filePath: string, content: string): Promise<void> { const dir = dirname(filePath); await this.ensureDirectoryExists(dir); try { await fs.writeFile(filePath, content, 'utf8'); } catch (error) { throw new FileOperationError(`Failed to write file`, filePath, 'write'); } } async readFile(filePath: string): Promise<string> { try { return await fs.readFile(filePath, 'utf8'); } catch (error) { if ((error as { code?: string }).code === 'ENOENT') { return ''; } throw new FileOperationError(`Failed to read file`, filePath, 'read'); } } async fileExists(filePath: string): Promise<boolean> { try { await fs.access(filePath); return true; } catch { return false; } } async getProjectDir(project?: string): Promise<string> { const projectRoot = await this.detectProjectRoot(); if (projectRoot) { return join(projectRoot, CONSTANTS.AI_LOGS_DIR); } // Fallback to home directory if no project detected const fallbackProject = project || 'default'; return join(this.homeConfigDir, 'projects', fallbackProject); } async getDailyLogPath(project: string, date: string): Promise<string> { const projectDir = await this.getProjectDir(project); return join(projectDir, `${date}.md`); } async getProjectConfigPath(project?: string): Promise<string> { const projectDir = await this.getProjectDir(project); return join(projectDir, 'config.json'); } getIndexDir(): string { return join(this.homeConfigDir, 'index'); } getConfigDir(): string { return join(this.homeConfigDir, 'config'); } async getProjectInfo(): Promise<{ name: string; root: string | null; logsDir: string }> { const projectRoot = await this.detectProjectRoot(); if (projectRoot) { return { name: this.getProjectNameFromPath(projectRoot), root: projectRoot, logsDir: join(projectRoot, CONSTANTS.AI_LOGS_DIR) }; } return { name: 'default', root: null, logsDir: join(this.homeConfigDir, 'projects', 'default') }; } async listProjects(): Promise<string[]> { // For the new design, we primarily work with the current project // But we can also scan the global registry for known projects const registryPath = join(this.getIndexDir(), 'projects-registry.json'); try { const registryContent = await this.readFile(registryPath); if (registryContent) { const registry = JSON.parse(registryContent); return Object.keys(registry); } } catch { // Registry doesn't exist or is corrupted } // If no registry, return current project if detected const projectInfo = await this.getProjectInfo(); if (projectInfo.root) { return [projectInfo.name]; } // Fallback: scan home config directory try { const projectsDir = join(this.homeConfigDir, 'projects'); const entries = await fs.readdir(projectsDir, { withFileTypes: true }); return entries .filter(entry => entry.isDirectory()) .map(entry => entry.name); } catch (error) { if ((error as { code?: string }).code === 'ENOENT') { return []; } const projectsDir = join(this.homeConfigDir, 'projects'); throw new FileOperationError(`Failed to list projects`, projectsDir, 'readdir'); } } async initializeProject(project?: string): Promise<void> { const projectInfo = await this.getProjectInfo(); const projectDir = await this.getProjectDir(project); await this.ensureDirectoryExists(projectDir); const configPath = await this.getProjectConfigPath(project); const configExists = await this.fileExists(configPath); if (!configExists) { const defaultConfig: ProjectConfig = { name: project || projectInfo.name, description: `AI conversation logs for project: ${project || projectInfo.name}`, defaultTags: [], autoTagging: true, exportSettings: { includeCode: true, includeActions: true } }; await this.writeFile(configPath, JSON.stringify(defaultConfig, null, 2)); } // Update global registry await this.updateProjectRegistry(project || projectInfo.name, projectInfo.root || projectDir); } private async updateProjectRegistry(projectName: string, projectPath: string): Promise<void> { const registryPath = join(this.getIndexDir(), 'projects-registry.json'); await this.ensureDirectoryExists(this.getIndexDir()); let registry: Record<string, { path: string; lastAccessed: string }> = {}; try { const registryContent = await this.readFile(registryPath); if (registryContent) { registry = JSON.parse(registryContent); } } catch { // Registry doesn't exist, start fresh } registry[projectName] = { path: projectPath, lastAccessed: new Date().toISOString() }; await this.writeFile(registryPath, JSON.stringify(registry, null, 2)); } }

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/fablefang/ai-conversation-logger-mcp'

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