@memory-bank/mcp
Version:
Memory-enabled Co-Pilot (MCP) server for managing project documentation and context
204 lines • 9.67 kB
JavaScript
import path from "path";
import fs from "fs/promises";
import { logger } from "../../../shared/utils/logger.js";
import { MemoryDocument } from "../../../domain/entities/MemoryDocument.js";
import { Tag } from "../../../domain/entities/Tag.js";
import { DomainError, DomainErrorCodes } from "../../../shared/errors/DomainError.js";
/**
* Handles document Input/Output operations within a specific branch directory.
* Assumes the branch directory already exists when methods are called.
*/
export class DocumentIO {
branchMemoryBankPath;
/**
* Constructor
* @param rootPath Root path for the memory bank (e.g., docs/)
*/
constructor(rootPath) {
this.branchMemoryBankPath = path.join(rootPath, 'branch-memory-bank');
logger.debug(`[DocumentIO] Initialized with rootPath: ${rootPath}`);
}
/**
* Gets the full path for a given document within a branch.
* @param branchInfo Branch information.
* @param documentPath Document path relative to the branch.
* @returns The full file path.
*/
getFullFilePath(branchInfo, documentPath) {
const safeBranchName = branchInfo.safeName;
const branchPath = path.join(this.branchMemoryBankPath, safeBranchName);
return path.join(branchPath, documentPath.value);
}
/**
* Checks if a file exists. (Currently unused, kept for potential future use)
* @param filePath Full file path
* @returns Promise resolving to boolean indicating if file exists
*/
/* // ★★★ 未使用のためコメントアウト ★★★
private async fileExists(filePath: string): Promise<boolean> {
logger.debug(`[DocumentIO] Checking file existence: ${filePath}`);
try {
await fs.access(filePath);
logger.debug('[DocumentIO] File exists:', { filePath });
return true;
} catch {
logger.debug('[DocumentIO] File does not exist:', { filePath });
return false;
}
}
*/ // ★★★ ここまで ★★★
/**
* Reads a document from the file system.
* @param branchInfo Branch information
* @param documentPath Document path
* @returns Promise resolving to MemoryDocument if found, null otherwise
*/
async getDocument(branchInfo, documentPath) {
const filePath = this.getFullFilePath(branchInfo, documentPath);
logger.debug(`[DocumentIO] Trying to read file:`, { filePath });
try {
const contentString = await fs.readFile(filePath, 'utf-8');
let tags = [];
let lastModified = new Date(); // Default value
if (documentPath.value.endsWith('.json')) {
try {
const parsedContent = JSON.parse(contentString);
if (parsedContent.metadata) {
if (Array.isArray(parsedContent.metadata.tags)) {
tags = parsedContent.metadata.tags
.filter((t) => typeof t === 'string' && t.trim() !== '')
.map((t) => Tag.create(t));
}
if (parsedContent.metadata.lastModified) {
try {
const parsedDate = new Date(parsedContent.metadata.lastModified);
if (!isNaN(parsedDate.getTime())) {
lastModified = parsedDate;
}
else {
logger.warn('[DocumentIO] Invalid lastModified date found, using current date.', { path: filePath, value: parsedContent.metadata.lastModified });
}
}
catch (dateError) {
logger.warn('[DocumentIO] Error parsing lastModified date, using current date.', { path: filePath, error: dateError });
}
}
}
}
catch (parseError) {
logger.warn('[DocumentIO] Failed to parse JSON content, returning default metadata.', { path: filePath, error: parseError });
}
}
return MemoryDocument.create({
path: documentPath,
content: contentString,
tags: tags,
lastModified: lastModified
});
}
catch (error) {
const isEnoent = error instanceof Error && error.code === 'ENOENT';
if (isEnoent) {
logger.debug(`[DocumentIO] Document not found (ENOENT): ${filePath}`);
return null;
}
logger.error(`[DocumentIO] Error reading document ${filePath}:`, error);
return null;
}
}
/**
* Saves a document to the file system.
* @param branchInfo Branch information
* @param document Document to save
* @returns Promise resolving when done
*/
async saveDocument(branchInfo, document) {
const filePath = this.getFullFilePath(branchInfo, document.path);
logger.debug('[DocumentIO] Saving document:', { filePath });
try {
let contentToSave;
let isJsonFile = document.path.value.endsWith('.json');
if (isJsonFile) {
try {
const parsedContent = JSON.parse(document.content || '{}');
if (typeof parsedContent.metadata !== 'object' || parsedContent.metadata === null) {
parsedContent.metadata = {};
}
parsedContent.metadata.tags = document.tags.map(t => t.value);
parsedContent.metadata.lastModified = document.lastModified.toISOString();
if (!parsedContent.metadata.path)
parsedContent.metadata.path = document.path.value;
if (!parsedContent.schema)
parsedContent.schema = 'memory_document_v2';
// Basic validation for branchContext.json (can be enhanced)
if (document.path.value === 'branchContext.json') {
const requiredKeys = ['schema', 'metadata', 'content'];
for (const key of requiredKeys) {
if (!(key in parsedContent)) {
throw new DomainError(DomainErrorCodes.INVALID_DOCUMENT_FORMAT, `Missing required key in branchContext.json: ${key}`);
}
}
}
contentToSave = JSON.stringify(parsedContent, null, 2);
logger.debug('[DocumentIO] Updated metadata before saving JSON file.');
}
catch (err) {
logger.error('[DocumentIO] Invalid JSON content provided for .json file:', {
error: err instanceof Error ? err.message : String(err),
filePath: filePath
});
throw new DomainError(DomainErrorCodes.INVALID_DOCUMENT_FORMAT, `Invalid JSON content for file ${document.path.value}`);
}
}
else {
contentToSave = document.content;
}
const parentDir = path.dirname(filePath);
await fs.mkdir(parentDir, { recursive: true });
logger.debug('[DocumentIO] Ensured parent directory exists:', { parentDir });
await fs.writeFile(filePath, contentToSave, 'utf-8');
logger.debug('[DocumentIO] Successfully wrote file:', { filePath });
}
catch (error) {
logger.error('[DocumentIO] Failed to save document:', { filePath, error });
throw new DomainError(DomainErrorCodes.REPOSITORY_ERROR, `Failed to save document ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Deletes a document from the file system.
* @param branchInfo Branch information
* @param documentPath Document path
* @returns Promise resolving to boolean indicating success
*/
async deleteDocument(branchInfo, documentPath) {
const filePath = this.getFullFilePath(branchInfo, documentPath);
logger.debug(`[DocumentIO] Attempting to delete document: ${filePath}`);
try {
await fs.unlink(filePath);
// Also delete corresponding .md file if .json is deleted
if (filePath.endsWith('.json')) {
const mdFilePath = filePath.replace('.json', '.md');
try {
await fs.unlink(mdFilePath);
logger.debug('[DocumentIO] Also deleted corresponding .md file:', { path: mdFilePath });
}
catch (mdError) {
if (!(mdError instanceof Error && mdError.code === 'ENOENT')) {
logger.warn('[DocumentIO] Error deleting corresponding .md file:', { path: mdFilePath, error: mdError });
}
}
}
return true;
}
catch (error) {
if (error instanceof Error && error.code === 'ENOENT') {
logger.warn(`[DocumentIO] Attempted to delete non-existent file: ${filePath}`);
}
else {
logger.error(`[DocumentIO] Error deleting file ${filePath}:`, error);
}
return false;
}
}
}
//# sourceMappingURL=DocumentIO.js.map