@endlessblink/like-i-said-v2
Version:
Task Management & Memory for Claude - Track tasks, remember context, and maintain continuity across sessions with 27 powerful tools. Works with Claude Desktop and Claude Code.
551 lines (462 loc) • 20.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { MemoryDescriptionQualityScorer } = require('./memory-description-quality-scorer.cjs');
// Skip MemoryFormat for now - implement simple parser
// const { MemoryFormat } = require('./memory-format');
// const { TitleSummaryGenerator } = require('./title-summary-generator');
/**
* Memory Task Automator
* Automated system for improving memory quality and descriptions
*/
class MemoryTaskAutomator {
constructor(storage, taskStorage, options = {}) {
this.storage = storage;
this.taskStorage = taskStorage;
this.options = {
enabled: true,
minConfidence: 0.5,
autoExecuteThreshold: 0.8,
logAutomatedActions: true,
...options
};
this.qualityScorer = new MemoryDescriptionQualityScorer();
// this.memoryFormat = new MemoryFormat();
// this.titleGenerator = new TitleSummaryGenerator();
}
/**
* Simple memory content parser
*/
parseMemoryContent(content) {
if (!content || typeof content !== 'string') return null;
// Try YAML frontmatter first
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---([\s\S]*)$/);
if (frontmatterMatch) {
return this.parseFrontmatter(frontmatterMatch[1], frontmatterMatch[2]);
}
// Try HTML comment metadata
const htmlMatch = content.match(/<!-- Memory Metadata\s*([\s\S]*?)\s*-->/);
if (htmlMatch) {
return this.parseHtmlComment(content, htmlMatch[1]);
}
// No metadata found - create basic structure
return {
content: content.trim(),
metadata: {
content_type: 'text',
size: content.length
}
};
}
/**
* Parse YAML frontmatter
*/
parseFrontmatter(frontmatter, bodyContent) {
const memory = {
content: bodyContent.trim(),
metadata: {},
format: 'yaml'
};
const lines = frontmatter.split(/\r?\n/);
lines.forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex === -1) return;
const key = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
// Handle arrays
if (key === 'tags' || key === 'related_memories') {
if (value.startsWith('[') && value.endsWith(']')) {
try {
memory[key] = JSON.parse(value);
} catch {
memory[key] = [];
}
} else {
memory[key] = value.split(',').map(t => t.trim()).filter(Boolean);
}
} else {
memory[key] = value;
}
});
return memory;
}
/**
* Parse HTML comment metadata
*/
parseHtmlComment(fullContent, metadataContent) {
const memory = {
content: fullContent.replace(/<!-- Memory Metadata\s*([\s\S]*?)\s*-->/, '').trim(),
metadata: {},
format: 'html-comment'
};
const lines = metadataContent.trim().split(/\r?\n/);
lines.forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex === -1) return;
const key = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
if (key === 'tags') {
memory[key] = value.split(',').map(t => t.trim()).filter(Boolean);
} else {
memory[key] = value;
}
});
return memory;
}
/**
* Format memory content back to markdown
*/
formatMemoryContent(memory) {
if (!memory) return '';
const frontmatter = [
'---',
memory.id ? `id: ${memory.id}` : null,
memory.timestamp ? `timestamp: ${memory.timestamp}` : null,
memory.complexity ? `complexity: ${memory.complexity}` : null,
memory.category ? `category: ${memory.category}` : null,
memory.project ? `project: ${memory.project}` : null,
memory.tags ? `tags: ${JSON.stringify(memory.tags)}` : null,
memory.priority ? `priority: ${memory.priority}` : null,
memory.status ? `status: ${memory.status}` : null,
'---',
''
].filter(line => line !== null).join('\n');
return frontmatter + (memory.content || '');
}
/**
* Process a single memory file for quality improvements
* @param {string} filePath - Path to the memory file
* @returns {Promise<Object>} Processing result
*/
async processMemoryFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
const parsed = this.parseMemoryContent(content);
const quality = this.qualityScorer.scoreMemoryQuality(parsed);
let improved = false;
const improvements = [];
// Skip if quality is already good
if (quality.totalScore >= 70) {
return {
path: filePath,
originalScore: quality.totalScore,
newScore: quality.totalScore,
improved: false,
skipped: true,
reason: 'Quality already good'
};
}
// Make improvements based on identified issues
const improvedMemory = { ...parsed };
// Fix title issues
if (quality.issues.some(issue => issue.type === 'title')) {
const newTitle = await this.generateImprovedTitle(parsed.content, parsed.metadata?.title);
if (newTitle && newTitle !== parsed.metadata?.title) {
improvedMemory.metadata = { ...improvedMemory.metadata, title: newTitle };
improvements.push('Fixed title');
improved = true;
}
}
// Fix description issues
if (quality.issues.some(issue => issue.type === 'description')) {
const enhancedContent = await this.enhanceContent(parsed.content);
if (enhancedContent && enhancedContent !== parsed.content) {
improvedMemory.content = enhancedContent;
improvements.push('Enhanced description');
improved = true;
}
}
// Fix metadata issues
if (quality.issues.some(issue => issue.type === 'metadata')) {
const improvedMetadata = this.improveMetadata(parsed.metadata);
if (improvedMetadata) {
improvedMemory.metadata = { ...improvedMemory.metadata, ...improvedMetadata };
improvements.push('Fixed metadata');
improved = true;
}
}
// Fix structure issues
if (quality.issues.some(issue => issue.type === 'structure')) {
const improvedStructure = this.improveStructure(improvedMemory.content);
if (improvedStructure !== improvedMemory.content) {
improvedMemory.content = improvedStructure;
improvements.push('Improved structure');
improved = true;
}
}
// Save improved memory if changes were made
if (improved) {
const newContent = this.formatMemoryContent(improvedMemory);
// Create backup before modifying
const backupPath = filePath + '.backup';
fs.copyFileSync(filePath, backupPath);
// Write improved version
fs.writeFileSync(filePath, newContent);
// Calculate new quality score
const newQuality = this.qualityScorer.scoreMemoryQuality(improvedMemory);
return {
path: filePath,
originalScore: quality.totalScore,
newScore: newQuality.totalScore,
improved: true,
improvements,
backupPath
};
}
return {
path: filePath,
originalScore: quality.totalScore,
newScore: quality.totalScore,
improved: false,
skipped: false,
reason: 'No improvements could be made'
};
} catch (error) {
return {
path: filePath,
error: error.message,
improved: false
};
}
}
/**
* Generate an improved title from content
*/
async generateImprovedTitle(content, currentTitle) {
if (!content || content.trim().length < 10) {
return currentTitle;
}
// Skip if current title is already good
if (currentTitle && currentTitle.length > 10 && !this.isTitlePoor(currentTitle)) {
return currentTitle;
}
try {
// Extract first meaningful sentence or heading
const lines = content.split('\n').filter(line => line.trim());
// Look for markdown headers
const headerMatch = lines.find(line => line.match(/^#{1,6}\s+(.+)/));
if (headerMatch) {
const title = headerMatch.replace(/^#{1,6}\s+/, '').trim();
if (title.length > 5 && title.length < 80) {
return title;
}
}
// Look for first meaningful sentence
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 5);
if (sentences.length > 0) {
let title = sentences[0].trim();
// Clean up common prefixes
title = title.replace(/^(this|that|here|there|the|a|an)\s+/i, '');
title = title.replace(/^(is|are|was|were|will|would|should|could)\s+/i, '');
// Capitalize first letter
title = title.charAt(0).toUpperCase() + title.slice(1);
// Truncate if too long
if (title.length > 80) {
title = title.substring(0, 77) + '...';
}
return title;
}
// Fallback to content summary
if (content.length > 20) {
const summary = content.substring(0, 77).trim();
return summary.charAt(0).toUpperCase() + summary.slice(1) + '...';
}
return currentTitle;
} catch (error) {
console.error('Error generating improved title:', error);
return currentTitle;
}
}
/**
* Check if a title is poor quality
*/
isTitlePoor(title) {
if (!title || title.trim().length < 5) return true;
const poorPatterns = [
/^title:/i,
/^memory/i,
/^note/i,
/^temp/i,
/^test/i,
/^-----/,
/\$\(date/,
/^id-\d+/,
/^untitled/i
];
return poorPatterns.some(pattern => pattern.test(title));
}
/**
* Enhance content quality
*/
async enhanceContent(content) {
if (!content || content.trim().length < 10) {
return content;
}
let enhanced = content;
// Fix common formatting issues
enhanced = enhanced.replace(/\n{3,}/g, '\n\n'); // Remove excessive newlines
enhanced = enhanced.replace(/\s+\n/g, '\n'); // Remove trailing spaces
enhanced = enhanced.replace(/\n\s+/g, '\n'); // Remove leading spaces after newlines
// Ensure proper sentence structure
const sentences = enhanced.split(/[.!?]+/).filter(s => s.trim().length > 0);
if (sentences.length > 0) {
enhanced = sentences.map(sentence => {
const trimmed = sentence.trim();
if (trimmed.length > 0) {
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
}
return trimmed;
}).join('. ');
// Ensure proper ending
if (!enhanced.match(/[.!?]$/)) {
enhanced += '.';
}
}
return enhanced;
}
/**
* Improve metadata quality
*/
improveMetadata(metadata) {
if (!metadata) return null;
const improvements = {};
// Add missing required fields
if (!metadata.category) {
improvements.category = 'personal'; // Default category
}
if (!metadata.priority) {
improvements.priority = 'medium'; // Default priority
}
if (!metadata.status) {
improvements.status = 'active'; // Default status
}
if (!metadata.complexity) {
improvements.complexity = 2; // Default complexity
}
// Fix invalid values
if (metadata.category && !['personal', 'work', 'code', 'research', 'conversations', 'preferences'].includes(metadata.category)) {
improvements.category = 'personal';
}
if (metadata.priority && !['low', 'medium', 'high'].includes(metadata.priority)) {
improvements.priority = 'medium';
}
if (metadata.complexity && (metadata.complexity < 1 || metadata.complexity > 4)) {
improvements.complexity = 2;
}
// Fix tags format
if (metadata.tags && !Array.isArray(metadata.tags)) {
if (typeof metadata.tags === 'string') {
improvements.tags = metadata.tags.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
} else {
improvements.tags = [];
}
}
return Object.keys(improvements).length > 0 ? improvements : null;
}
/**
* Improve content structure
*/
improveStructure(content) {
if (!content || content.length < 100) {
return content;
}
let improved = content;
// Add headers for longer content without any
if (content.length > 300 && !content.includes('#')) {
const lines = content.split('\n');
const firstLine = lines[0].trim();
if (firstLine.length > 5 && firstLine.length < 80) {
improved = `# ${firstLine}\n\n${lines.slice(1).join('\n')}`;
}
}
// Format code blocks properly
improved = improved.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const language = lang || '';
const cleanCode = code.trim();
return `\`\`\`${language}\n${cleanCode}\n\`\`\``;
});
// Ensure proper list formatting
improved = improved.replace(/^[\s]*[-*+]\s+/gm, '- ');
return improved;
}
/**
* Process all memory files in a directory
*/
async processAllMemories(memoriesPath = 'memories') {
const results = {
totalProcessed: 0,
improved: 0,
skipped: 0,
errors: 0,
improvements: [],
errorDetails: []
};
const processDirectory = async (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
await processDirectory(fullPath);
} else if (entry.name.endsWith('.md')) {
results.totalProcessed++;
const result = await this.processMemoryFile(fullPath);
if (result.error) {
results.errors++;
results.errorDetails.push(result);
} else if (result.improved) {
results.improved++;
results.improvements.push(result);
} else {
results.skipped++;
}
// Log progress
if (results.totalProcessed % 10 === 0) {
console.log(`Processed ${results.totalProcessed} files...`);
}
}
}
};
if (fs.existsSync(memoriesPath)) {
await processDirectory(memoriesPath);
}
return results;
}
/**
* Generate a quality improvement report
*/
async generateImprovementReport(memoriesPath = 'memories') {
const beforeReport = await this.qualityScorer.generateBulkQualityReport(memoriesPath);
const processingResults = await this.processAllMemories(memoriesPath);
const afterReport = await this.qualityScorer.generateBulkQualityReport(memoriesPath);
return {
before: beforeReport,
processing: processingResults,
after: afterReport,
summary: {
totalMemories: beforeReport.totalMemories,
processedCount: processingResults.totalProcessed,
improvedCount: processingResults.improved,
skippedCount: processingResults.skipped,
errorCount: processingResults.errors,
scoreImprovement: {
averageBefore: this.calculateAverageScore(beforeReport.qualityDistribution),
averageAfter: this.calculateAverageScore(afterReport.qualityDistribution),
improvement: this.calculateAverageScore(afterReport.qualityDistribution) -
this.calculateAverageScore(beforeReport.qualityDistribution)
}
}
};
}
/**
* Calculate average quality score from distribution
*/
calculateAverageScore(distribution) {
const scoreMap = { excellent: 95, good: 80, fair: 65, poor: 50, very_poor: 25 };
const totalItems = Object.values(distribution).reduce((sum, count) => sum + count, 0);
if (totalItems === 0) return 0;
const totalScore = Object.entries(distribution).reduce((sum, [level, count]) => {
return sum + (scoreMap[level] * count);
}, 0);
return Math.round(totalScore / totalItems);
}
}
module.exports = { MemoryTaskAutomator };