UNPKG

@memory-bank/mcp

Version:

Memory-enabled Co-Pilot (MCP) server for managing project documentation and context

316 lines 11.6 kB
import { Tag } from './Tag.js'; import { DomainError } from '../../shared/errors/DomainError.js'; import crypto from 'crypto'; // crypto をインポート /** * Static logger instance for MemoryDocument * This is set from outside to avoid domain entities depending directly on infrastructure */ let documentLogger = null; /** * Set the logger instance for MemoryDocument * This allows dependency injection from outside the domain * @param logger Logger implementation to use */ export function setDocumentLogger(logger) { documentLogger = logger; } /** * Get the current logger or use a no-op logger if not set * @returns The current logger or a no-op logger */ function getLogger() { if (!documentLogger) { return { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } }; } return documentLogger; } /** * Entity representing a document in the memory bank */ export class MemoryDocument { props; constructor(props) { this.props = props; } /** * Factory method to create a new MemoryDocument * @param props Document properties * @returns MemoryDocument instance */ static create(props) { const documentProps = { path: props.path, content: props.content, tags: [...props.tags], lastModified: new Date(props.lastModified), }; return new MemoryDocument(documentProps); } /** * Get the document path */ get path() { return this.props.path; } /** * Get the document content */ get content() { return this.props.content; } /** * Get the document tags */ get tags() { return [...this.props.tags]; } /** * Get the last modified date */ get lastModified() { return new Date(this.props.lastModified); } /** * Check if document has a specific tag * @param tag Tag to check * @returns boolean indicating if document has tag */ hasTag(tag) { getLogger().debug('Checking tag:', { documentTags: this.props.tags.map(t => t.value), searchTag: tag.value }); const hasTag = this.props.tags.some((t) => { const matches = t.equals(tag); getLogger().debug('Tag comparison:', { tag1: t.value, tag2: tag.value, matches }); return matches; }); return hasTag; } /** * Create a new document with updated content * @param content New content * @returns New MemoryDocument instance */ updateContent(content) { if (content === this.props.content) { return this; } return MemoryDocument.create({ ...this.props, content, lastModified: new Date(), }); } /** * Create a new document with added tag * @param tag Tag to add * @returns New MemoryDocument instance */ addTag(tag) { if (this.hasTag(tag)) { return this; } return MemoryDocument.create({ ...this.props, tags: [...this.props.tags, tag], lastModified: new Date(), }); } /** * Create a new document with removed tag * @param tag Tag to remove * @returns New MemoryDocument instance */ removeTag(tag) { if (!this.hasTag(tag)) { return this; } return MemoryDocument.create({ ...this.props, tags: this.props.tags.filter((t) => !t.equals(tag)), lastModified: new Date(), }); } /** * Create a new document with updated tags * @param tags New tags * @returns New MemoryDocument instance */ updateTags(tags) { return MemoryDocument.create({ ...this.props, tags, lastModified: new Date(), }); } /** * Extract the document title from content * @returns Document title or undefined if not found */ get title() { const lines = this.props.content.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('# ')) { return trimmedLine.substring(2).trim(); } } // If JSON, try to parse and get metadata.title if (this.isJSON) { try { const parsed = JSON.parse(this.props.content); if (parsed && parsed.metadata && typeof parsed.metadata.title === 'string') { return parsed.metadata.title; } } catch { // Ignore parsing errors for title extraction } } return undefined; } /** * Check if the document is a JSON file */ get isJSON() { return this.props.path.isJSON; } /** * Convert to plain object for serialization */ toObject() { return { path: this.props.path.value, content: this.props.content, tags: this.props.tags.map((tag) => tag.value), lastModified: this.props.lastModified.toISOString(), }; } /** * Convert document to JSON format * @returns JSON document object */ toJSON() { if (this.isJSON) { // <<<--- if ブロックを復活! try { // ファイルの内容をパース const parsedContent = JSON.parse(this.props.content); // スキーマ v2 形式であることを確認し、そのまま返す // (ここで Zod などでバリデーションするのがより堅牢) if (parsedContent.schema === 'memory_document_v2' && parsedContent.documentType && parsedContent.metadata && parsedContent.content) { // パースした内容をそのまま返す (ID などもファイルの内容を維持) // 必要であれば、lastModified や tags を props の値で上書きする // parsedContent.metadata.lastModified = this.props.lastModified.toISOString(); // parsedContent.metadata.tags = this.props.tags.map(tag => tag.value); return parsedContent; } else { getLogger().warn('Parsed JSON content does not match expected v2 schema, falling back to reconstruction.', { path: this.props.path.value }); // スキーマが不正な場合は、後半の組み立てロジックにフォールバック } } catch (error) { getLogger().error('Failed to parse JSON document, falling back to reconstruction:', { error, path: this.props.path.value }); // パースエラーの場合も、後半の組み立てロジックにフォールバック } } // --- JSON 形式でない場合、またはパース/バリデーション失敗時のフォールバック --- const documentType = this.determineDocumentType(); // <<<--- 呼び出しはここ const title = this.title || this.props.path.filename; // metadata オブジェクトを作成 (documentType を除く) const metadata = { id: crypto.randomUUID(), // 新しいIDを生成 (JSONでない場合はこれで良い) title: title, path: this.props.path.value, tags: this.props.tags.map((tag) => tag.value), lastModified: this.props.lastModified.toISOString(), createdAt: new Date().toISOString(), // 作成日時は常に現在時刻 version: 1, }; // content オブジェクトを作成 (JSONでない場合の簡易的な構造) let content = { text: this.props.content, // デフォルトは text フィールドに入れる }; // 必要に応じて documentType ごとの特殊な content 構造を定義 // (例: branch_context なら purpose に入れるなど、ただし今回は text に統一) // スキーマ v2 形式で返す (documentType をトップレベルに) return { schema: 'memory_document_v2', documentType: documentType, // <<<--- トップレベルに追加! metadata: metadata, content: content, }; // 型アサーションは維持 (必要に応じて調整) } /** * Convert JSON document to Markdown * @param jsonDoc JSON document to convert * @returns Markdown formatted string */ static fromJSON(jsonDoc, path) { getLogger().debug('Creating MemoryDocument from JSON:', { path: path.value, schema: jsonDoc.schema }); const lastModified = jsonDoc.metadata.lastModified; // Access via metadata const sanitizedTags = (jsonDoc.metadata.tags || []).map((tag) => { try { const tagObj = Tag.create(tag); getLogger().debug('Created tag:', { tag: tagObj.value, source: tag }); return tagObj; } catch (e) { if (e instanceof DomainError && e.code === 'DOMAIN_ERROR.INVALID_TAG_FORMAT') { const sanitizedTagStr = tag.toLowerCase().replace(/[^a-z0-9-]/g, '-'); getLogger().warn(`Sanitized tag '${tag}' to '${sanitizedTagStr}'`); const tagObj = Tag.create(sanitizedTagStr); getLogger().debug('Created sanitized tag:', { tag: tagObj.value }); return tagObj; } throw e; } }); const doc = MemoryDocument.create({ path, content: JSON.stringify(jsonDoc, null, 2), tags: sanitizedTags, lastModified: new Date(lastModified), }); getLogger().debug('Created MemoryDocument with tags:', { tags: doc.tags.map(t => t.value) }); return doc; } /** * Determine the document type based on the path or content * @returns document type */ determineDocumentType() { const filename = this.props.path.filename.toLowerCase(); if (filename.includes('branchcontext') || filename.includes('branch-context')) { return 'branch_context'; } else if (filename.includes('activecontext') || filename.includes('active-context')) { return 'active_context'; } else if (filename.includes('progress')) { return 'progress'; } else if (filename.includes('systempatterns') || filename.includes('system-patterns')) { return 'system_patterns'; } else { // JsonDocumentV2 に 'generic' はないので、デフォルトの型を返す // (本来はどう扱うべきか要検討) // ここで適切なデフォルトタイプを返すか、エラーを投げるべき // 例として 'branch_context' を返す (スキーマ定義に合わせて調整が必要) getLogger().warn(`Could not determine specific document type for ${filename}, defaulting.`); // return 'generic'; // スキーマに generic がない場合はエラーになる return 'branch_context'; // 仮のデフォルト } } } //# sourceMappingURL=MemoryDocument.js.map