@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.
442 lines (387 loc) • 14.9 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { MemoryFormat } from './memory-format.js';
export class DropoffGenerator {
constructor(baseDir = null, packageJsonPath = 'package.json') {
this.baseDir = baseDir || process.env.MEMORY_DIR || 'memories';
this.packageJsonPath = packageJsonPath;
this.memoryFormat = new MemoryFormat();
}
/**
* Generate a comprehensive session dropoff document
* @param {Object} options - Configuration options
* @param {string} options.sessionSummary - Brief summary of work done
* @param {boolean} options.includeRecentMemories - Include recent memories (default: true)
* @param {boolean} options.includeGitStatus - Include git status (default: true)
* @param {number} options.recentMemoryCount - Number of recent memories to include (default: 5)
* @param {string} options.outputFormat - Output format: 'markdown' or 'json' (default: 'markdown')
* @returns {string} Generated dropoff content
*/
async generateDropoff(options = {}) {
const config = {
sessionSummary: options.sessionSummary || 'Session work completed',
includeRecentMemories: options.includeRecentMemories !== false,
includeGitStatus: options.includeGitStatus !== false,
recentMemoryCount: options.recentMemoryCount || 5,
outputFormat: options.outputFormat || 'markdown',
...options
};
try {
const contextData = await this.collectContextData(config);
if (config.outputFormat === 'json') {
return JSON.stringify(contextData, null, 2);
}
return this.generateMarkdownDropoff(contextData, config);
} catch (error) {
console.error('Error generating dropoff:', error);
throw error;
}
}
/**
* Collect all context data for the dropoff
*/
async collectContextData(config) {
const contextData = {
timestamp: new Date().toISOString(),
sessionSummary: config.sessionSummary,
projectInfo: await this.getProjectInfo(),
gitStatus: config.includeGitStatus ? await this.getGitStatus() : null,
recentMemories: config.includeRecentMemories ? await this.getRecentMemories(config.recentMemoryCount) : [],
systemStatus: await this.getSystemStatus(),
nextSteps: await this.detectNextSteps()
};
return contextData;
}
/**
* Get project information from package.json
*/
async getProjectInfo() {
try {
if (!fs.existsSync(this.packageJsonPath)) {
return { error: 'package.json not found' };
}
const packageJson = JSON.parse(fs.readFileSync(this.packageJsonPath, 'utf8'));
const currentDir = process.cwd();
return {
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
location: currentDir,
repository: packageJson.repository?.url || 'Not specified',
scripts: Object.keys(packageJson.scripts || {})
};
} catch (error) {
return { error: `Failed to read project info: ${error.message}` };
}
}
/**
* Get git status information
*/
async getGitStatus() {
try {
const status = {
currentBranch: this.runGitCommand('git branch --show-current').trim(),
hasChanges: false,
unstagedFiles: [],
stagedFiles: [],
untrackedFiles: [],
recentCommits: []
};
// Check for changes
const gitStatus = this.runGitCommand('git status --porcelain');
if (gitStatus) {
status.hasChanges = true;
const lines = gitStatus.split('\n').filter(line => line.trim());
lines.forEach(line => {
const statusCode = line.substring(0, 2);
const filePath = line.substring(3);
if (statusCode.includes('??')) {
status.untrackedFiles.push(filePath);
} else if (statusCode[0] !== ' ') {
status.stagedFiles.push(filePath);
} else if (statusCode[1] !== ' ') {
status.unstagedFiles.push(filePath);
}
});
}
// Get recent commits
const recentCommits = this.runGitCommand('git log --oneline -5');
status.recentCommits = recentCommits.split('\n').filter(line => line.trim());
return status;
} catch (error) {
return { error: `Git not available or not a git repository: ${error.message}` };
}
}
/**
* Get recent memories
*/
async getRecentMemories(count = 5) {
try {
const memories = [];
const categories = await this.getMemoryCategories();
// Collect all memory files with timestamps
const allMemories = [];
for (const category of categories) {
const categoryPath = path.join(this.baseDir, category);
if (fs.existsSync(categoryPath)) {
const files = fs.readdirSync(categoryPath).filter(file => file.endsWith('.md'));
for (const file of files) {
const filePath = path.join(categoryPath, file);
const stats = fs.statSync(filePath);
try {
const content = fs.readFileSync(filePath, 'utf8');
const parsed = this.memoryFormat.parseMemory(content);
allMemories.push({
file: file,
category: category,
title: parsed.metadata.title || file.replace('.md', ''),
timestamp: parsed.metadata.timestamp || stats.mtime.toISOString(),
tags: parsed.metadata.tags || [],
priority: parsed.metadata.priority || 'medium',
mtime: stats.mtime
});
} catch (parseError) {
// If parsing fails, include basic file info
allMemories.push({
file: file,
category: category,
title: file.replace('.md', ''),
timestamp: stats.mtime.toISOString(),
tags: [],
priority: 'medium',
mtime: stats.mtime,
parseError: true
});
}
}
}
}
// Sort by modification time (most recent first) and take top N
allMemories.sort((a, b) => b.mtime - a.mtime);
return allMemories.slice(0, count);
} catch (error) {
return [{ error: `Failed to get recent memories: ${error.message}` }];
}
}
/**
* Get memory categories
*/
async getMemoryCategories() {
try {
if (!fs.existsSync(this.baseDir)) {
return [];
}
return fs.readdirSync(this.baseDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
} catch (error) {
return [];
}
}
/**
* Get system status information
*/
async getSystemStatus() {
try {
const status = {
nodeVersion: process.version,
platform: process.platform,
workingDirectory: process.cwd(),
memoryUsage: process.memoryUsage(),
uptime: process.uptime(),
ports: {
dashboard: 5173, // Vite default
api: 3001 // Express API
}
};
// Check if processes are running (simplified check)
try {
const netstat = this.runCommand('netstat -an');
status.ports.dashboardRunning = netstat.includes(':5173');
status.ports.apiRunning = netstat.includes(':3001');
} catch (error) {
status.ports.checkError = 'Could not check port status';
}
return status;
} catch (error) {
return { error: `Failed to get system status: ${error.message}` };
}
}
/**
* Detect suggested next steps based on recent activity
*/
async detectNextSteps() {
const suggestions = [];
try {
// Check if there are uncommitted changes
const gitStatus = await this.getGitStatus();
if (gitStatus && !gitStatus.error && gitStatus.hasChanges) {
suggestions.push('Review and commit pending changes');
}
// Check recent memories for patterns
const recentMemories = await this.getRecentMemories(3);
if (recentMemories.length > 0) {
const recentTags = recentMemories.flatMap(m => m.tags || []);
const commonTags = this.findMostFrequent(recentTags, 2);
if (commonTags.length > 0) {
suggestions.push(`Continue work on: ${commonTags.join(', ')}`);
}
}
// Standard suggestions
suggestions.push('Test the WebSocket updates with actual MCP memory creation');
suggestions.push('Verify dashboard shows real-time updates');
suggestions.push('Run npm run migrate if needed for memory format consistency');
return suggestions;
} catch (error) {
return ['Review project status and continue development'];
}
}
/**
* Generate markdown formatted dropoff document
*/
generateMarkdownDropoff(contextData, config) {
const { projectInfo, gitStatus, recentMemories, systemStatus, nextSteps } = contextData;
let markdown = `# ${projectInfo.name || 'Project'} - Session Drop-off\n\n`;
markdown += `## Quick Copy-Paste Prompt for New Session\n\n`;
markdown += '```\n';
markdown += `Continue working on ${projectInfo.name || 'the project'} from where we left off.\n\n`;
markdown += `Project location: ${projectInfo.location}\n`;
markdown += `Current version: ${projectInfo.version}\n\n`;
// Session summary
markdown += `Session Summary: ${contextData.sessionSummary}\n\n`;
// Recent memories section
if (recentMemories && recentMemories.length > 0) {
markdown += 'Recent work:\n';
recentMemories.slice(0, 3).forEach((memory, index) => {
markdown += `${index + 1}. ${memory.title} (${memory.category})\n`;
});
markdown += '\n';
}
// Git status
if (gitStatus && !gitStatus.error) {
markdown += `Current branch: ${gitStatus.currentBranch}\n`;
if (gitStatus.hasChanges) {
markdown += 'Status: Has uncommitted changes\n';
} else {
markdown += 'Status: Clean working directory\n';
}
markdown += '\n';
}
// Next steps
if (nextSteps && nextSteps.length > 0) {
markdown += 'Next priorities:\n';
nextSteps.slice(0, 4).forEach((step, index) => {
markdown += `${index + 1}. ${step}\n`;
});
markdown += '\n';
}
markdown += 'Quick verification:\n';
markdown += `cd ${projectInfo.location}\n`;
if (projectInfo.scripts && projectInfo.scripts.includes('dev:full')) {
markdown += 'npm run dev:full\n';
} else if (projectInfo.scripts && projectInfo.scripts.includes('dev')) {
markdown += 'npm run dev\n';
}
markdown += '```\n\n';
// Detailed sections
markdown += '## Detailed Context\n\n';
// Project info
markdown += '### Project Information\n\n';
markdown += `- **Name**: ${projectInfo.name}\n`;
markdown += `- **Version**: ${projectInfo.version}\n`;
markdown += `- **Description**: ${projectInfo.description}\n`;
markdown += `- **Location**: ${projectInfo.location}\n`;
if (projectInfo.repository && !projectInfo.repository.includes('Not specified')) {
markdown += `- **Repository**: ${projectInfo.repository}\n`;
}
markdown += '\n';
// Git status details
if (gitStatus && !gitStatus.error) {
markdown += '### Git Status\n\n';
markdown += `- **Branch**: ${gitStatus.currentBranch}\n`;
markdown += `- **Has Changes**: ${gitStatus.hasChanges ? 'Yes' : 'No'}\n`;
if (gitStatus.unstagedFiles.length > 0) {
markdown += `- **Modified Files**: ${gitStatus.unstagedFiles.join(', ')}\n`;
}
if (gitStatus.stagedFiles.length > 0) {
markdown += `- **Staged Files**: ${gitStatus.stagedFiles.join(', ')}\n`;
}
if (gitStatus.untrackedFiles.length > 0) {
markdown += `- **Untracked Files**: ${gitStatus.untrackedFiles.join(', ')}\n`;
}
if (gitStatus.recentCommits.length > 0) {
markdown += '\n**Recent Commits**:\n';
gitStatus.recentCommits.forEach(commit => {
markdown += `- ${commit}\n`;
});
}
markdown += '\n';
}
// Recent memories details
if (recentMemories && recentMemories.length > 0) {
markdown += '### Recent Memories\n\n';
recentMemories.forEach((memory, index) => {
markdown += `${index + 1}. **${memory.title}** (${memory.category})\n`;
markdown += ` - File: ${memory.file}\n`;
markdown += ` - Timestamp: ${memory.timestamp}\n`;
if (memory.tags && memory.tags.length > 0) {
markdown += ` - Tags: ${memory.tags.join(', ')}\n`;
}
markdown += ` - Priority: ${memory.priority}\n`;
if (memory.parseError) {
markdown += ` - Note: Parse error, showing basic info\n`;
}
markdown += '\n';
});
}
// System status
if (systemStatus && !systemStatus.error) {
markdown += '### System Status\n\n';
markdown += `- **Node Version**: ${systemStatus.nodeVersion}\n`;
markdown += `- **Platform**: ${systemStatus.platform}\n`;
markdown += `- **Working Directory**: ${systemStatus.workingDirectory}\n`;
if (systemStatus.ports) {
markdown += `- **Dashboard Port**: ${systemStatus.ports.dashboard}\n`;
markdown += `- **API Port**: ${systemStatus.ports.api}\n`;
}
markdown += '\n';
}
markdown += `---\n\n`;
markdown += `*Generated on ${new Date(contextData.timestamp).toLocaleString()}*\n`;
return markdown;
}
/**
* Run git command safely
*/
runGitCommand(command) {
try {
return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
throw error;
}
}
/**
* Run system command safely
*/
runCommand(command) {
try {
return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
throw error;
}
}
/**
* Find most frequent items in array
*/
findMostFrequent(arr, count = 3) {
const frequency = {};
arr.forEach(item => {
frequency[item] = (frequency[item] || 0) + 1;
});
return Object.entries(frequency)
.sort((a, b) => b[1] - a[1])
.slice(0, count)
.map(entry => entry[0]);
}
}