@tosin2013/mcp-shrimp-task-manager
Version:
Enhanced MCP Shrimp Task Manager with comprehensive LLM integration. A task management tool built for AI Agents, emphasizing chain-of-thought, reflection, and style consistency. Features real GPT-4 ↔ MCP tools communication, comprehensive testing pipeline
335 lines • 12.6 kB
JavaScript
/**
* File system utilities for the Idea Honing Tool
*
* These utilities handle reading, writing, and managing specification files and related data.
*/
import fsPromises from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// Get the base directory for storing specifications
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, '../../../..');
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, 'data');
const SPECS_DIR = path.join(DATA_DIR, 'specifications');
/**
* Creates the necessary directory structure for storing specifications
*
* @param projectId - Optional project identifier for project-specific directories
* @returns Promise that resolves when directories are created
*/
export async function createDirectoryStructure(projectId) {
try {
// Create the main specifications directory if it doesn't exist
await fsPromises.mkdir(SPECS_DIR, { recursive: true });
// Create project-specific directory if a project ID is provided
if (projectId) {
const projectDir = path.join(SPECS_DIR, projectId);
await fsPromises.mkdir(projectDir, { recursive: true });
}
// Create directories for backups and metadata
await fsPromises.mkdir(path.join(SPECS_DIR, 'backups'), { recursive: true });
await fsPromises.mkdir(path.join(SPECS_DIR, 'metadata'), { recursive: true });
}
catch (error) {
throw new Error(`Failed to create directory structure: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Gets the path for a specification file
*
* @param specId - Specification identifier
* @param projectId - Optional project identifier
* @returns Path to the specification file
*/
export function getSpecificationPath(specId, projectId) {
if (projectId) {
return path.join(SPECS_DIR, projectId, `${specId}.md`);
}
return path.join(SPECS_DIR, `${specId}.md`);
}
/**
* Gets the path for a specification metadata file
*
* @param specId - Specification identifier
* @returns Path to the metadata file
*/
export function getMetadataPath(specId) {
return path.join(SPECS_DIR, 'metadata', `${specId}.json`);
}
/**
* Gets the path for a specification backup file
*
* @param specId - Specification identifier
* @param version - Version number
* @returns Path to the backup file
*/
export function getBackupPath(specId, version) {
return path.join(SPECS_DIR, 'backups', `${specId}_v${version}.md`);
}
/**
* Saves a specification document to the file system
*
* @param spec - Specification document to save
* @returns Promise that resolves when the specification is saved
*/
export async function saveSpecification(spec) {
try {
// Ensure the directory structure exists
await createDirectoryStructure(spec.projectId);
// Generate the markdown content from the specification
const markdownContent = generateMarkdownFromSpec(spec);
// Save the markdown content to the specification file
const specPath = getSpecificationPath(spec.id, spec.projectId);
await fsPromises.writeFile(specPath, markdownContent, 'utf-8');
// Save the metadata to a separate JSON file
const metadataPath = getMetadataPath(spec.id);
const metadata = {
id: spec.id,
title: spec.title,
metadata: spec.metadata,
version: spec.version,
projectId: spec.projectId,
featureId: spec.featureId,
createdAt: spec.createdAt,
updatedAt: spec.updatedAt,
};
await fsPromises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
// Create a backup of this version
await createBackup(spec);
}
catch (error) {
throw new Error(`Failed to save specification: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Loads a specification document from the file system
*
* @param specId - Specification identifier
* @param projectId - Optional project identifier
* @returns Promise that resolves with the loaded specification
*/
export async function loadSpecification(specId, projectId) {
try {
// Load the metadata file
const metadataPath = getMetadataPath(specId);
const metadataContent = await fsPromises.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(metadataContent);
// Load the specification file
const specPath = getSpecificationPath(specId, projectId);
const markdownContent = await fsPromises.readFile(specPath, 'utf-8');
// Parse the markdown content into sections
const sections = parseMarkdownToSections(markdownContent);
// Load the change history
const changeHistory = await loadChangeHistory(specId);
// Construct and return the complete specification document
return {
id: metadata.id,
title: metadata.title,
sections,
metadata: metadata.metadata,
version: metadata.version,
changeHistory,
projectId: metadata.projectId,
featureId: metadata.featureId,
createdAt: new Date(metadata.createdAt),
updatedAt: new Date(metadata.updatedAt),
};
}
catch (error) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
throw new Error(`Specification not found: ${specId}`);
}
throw new Error(`Failed to load specification: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Creates a backup of a specification
*
* @param spec - Specification document to backup
* @returns Promise that resolves when the backup is created
*/
export async function createBackup(spec) {
try {
// Generate the markdown content from the specification
const markdownContent = generateMarkdownFromSpec(spec);
// Save the backup file
const backupPath = getBackupPath(spec.id, spec.version);
await fsPromises.writeFile(backupPath, markdownContent, 'utf-8');
}
catch (error) {
throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Loads the change history for a specification
*
* @param specId - Specification identifier
* @returns Promise that resolves with the change history
*/
export async function loadChangeHistory(specId) {
try {
const historyPath = path.join(SPECS_DIR, 'metadata', `${specId}_history.json`);
// Check if the history file exists
try {
await fsPromises.access(historyPath);
}
catch {
// If the file doesn't exist, return an empty history
return [];
}
// Load and parse the history file
const historyContent = await fsPromises.readFile(historyPath, 'utf-8');
const history = JSON.parse(historyContent);
// Convert string timestamps to Date objects
return history.map((record) => ({
...record,
timestamp: new Date(record.timestamp),
}));
}
catch (error) {
throw new Error(`Failed to load change history: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Saves a change record to the change history
*
* @param specId - Specification identifier
* @param changeRecord - Change record to save
* @returns Promise that resolves when the change record is saved
*/
export async function saveChangeRecord(specId, changeRecord) {
try {
const historyPath = path.join(SPECS_DIR, 'metadata', `${specId}_history.json`);
// Load existing history or create a new one
let history = [];
try {
const historyContent = await fsPromises.readFile(historyPath, 'utf-8');
history = JSON.parse(historyContent);
}
catch (error) {
// If the file doesn't exist or can't be parsed, start with an empty history
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
console.error(`Error reading change history: ${error}`);
}
}
// Add the new change record
history.push(changeRecord);
// Save the updated history
await fsPromises.writeFile(historyPath, JSON.stringify(history, null, 2), 'utf-8');
}
catch (error) {
throw new Error(`Failed to save change record: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Generates markdown content from a specification document
*
* @param spec - Specification document
* @returns Markdown content
*/
function generateMarkdownFromSpec(spec) {
// Start with the title
let markdown = `# ${spec.title}\n\n`;
// Sort sections by order
const sortedSections = [...spec.sections].sort((a, b) => a.order - b.order);
// Add each section
for (const section of sortedSections) {
markdown += `## ${section.title}\n\n${section.content}\n\n`;
}
// Add metadata at the end
markdown += `<!-- Metadata: ${JSON.stringify({
id: spec.id,
version: spec.version,
updatedAt: spec.updatedAt,
})} -->\n`;
return markdown;
}
/**
* Parses markdown content into specification sections
*
* @param markdown - Markdown content
* @returns Array of specification sections
*/
function parseMarkdownToSections(markdown) {
// Simple parsing logic - this would be enhanced in a real implementation
const sections = [];
const lines = markdown.split('\n');
let currentSection = null;
let currentContent = '';
for (const line of lines) {
// Skip metadata line
if (line.startsWith('<!-- Metadata:'))
continue;
// Check for section headers
if (line.startsWith('## ')) {
// Save the previous section if there is one
if (currentSection) {
currentSection.content = currentContent.trim();
sections.push(currentSection);
}
// Start a new section
const title = line.substring(3).trim();
currentSection = {
id: title.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
title,
content: '',
order: sections.length,
};
currentContent = '';
}
else if (currentSection) {
// Add the line to the current section content
currentContent += line + '\n';
}
}
// Save the last section
if (currentSection) {
currentSection.content = currentContent.trim();
sections.push(currentSection);
}
return sections;
}
/**
* Lists all specifications in the system
*
* @param projectId - Optional project identifier to filter by
* @returns Promise that resolves with an array of specification metadata
*/
export async function listSpecifications(projectId) {
try {
const metadataDir = path.join(SPECS_DIR, 'metadata');
// Ensure the metadata directory exists
try {
await fsPromises.access(metadataDir);
}
catch {
// If the directory doesn't exist, return an empty array
return [];
}
// Get all metadata files
const files = await fsPromises.readdir(metadataDir);
const metadataFiles = files.filter(file => file.endsWith('.json') && !file.includes('_history'));
// Load metadata for each specification
const specifications = [];
for (const file of metadataFiles) {
try {
const metadataPath = path.join(metadataDir, file);
const metadataContent = await fsPromises.readFile(metadataPath, 'utf-8');
const metadata = JSON.parse(metadataContent);
// Filter by project ID if provided
if (!projectId || metadata.projectId === projectId) {
specifications.push(metadata);
}
}
catch (error) {
console.error(`Error loading metadata from ${file}: ${error}`);
}
}
return specifications;
}
catch (error) {
throw new Error(`Failed to list specifications: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=file-system.js.map