UNPKG

lamplighter-mcp

Version:

An intelligent context engine for AI-assisted software development

191 lines (156 loc) 5.99 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); const DEFAULT_CONTEXT_DIR = './lamplighter_context'; const FEATURE_TASKS_DIR = 'feature_tasks'; // Define task status types export type TaskStatus = 'ToDo' | 'InProgress' | 'Done'; // Interface for parsed task objects interface Task { text: string; status: TaskStatus; lineNumber: number; originalLine: string; } export class TaskManager { private contextDir: string; private featureTasksDir: string; constructor() { this.contextDir = process.env.LAMPLIGHTER_CONTEXT_DIR || DEFAULT_CONTEXT_DIR; this.featureTasksDir = path.join(this.contextDir, FEATURE_TASKS_DIR); console.log(`[TaskManager] Initialized. Task files located in: ${this.featureTasksDir}`); } /** * Update the status of a task */ async updateTaskStatus( featureIdentifier: string, taskIdentifier: string, newStatus: TaskStatus ): Promise<void> { try { // Build the path to the feature task file const filePath = path.join(this.featureTasksDir, `feature_${featureIdentifier}_tasks.md`); // Check if file exists try { await fs.access(filePath); } catch (error) { throw new Error(`Task file for feature "${featureIdentifier}" not found.`); } // Read the file content const content = await fs.readFile(filePath, 'utf-8'); // Parse tasks from the content const tasks = this.parseTasks(content); // Find the task by identifier (text match) const taskIndex = tasks.findIndex(task => // Check for exact match or if the task text contains the identifier task.text === taskIdentifier || task.text.includes(taskIdentifier) ); if (taskIndex === -1) { throw new Error(`Task "${taskIdentifier}" not found in feature "${featureIdentifier}".`); } // Update the task's status const updatedContent = this.updateTaskInContent(content, tasks[taskIndex], newStatus); // Write the updated content back to the file await fs.writeFile(filePath, updatedContent, 'utf-8'); console.log(`[TaskManager] Updated task "${taskIdentifier}" in feature "${featureIdentifier}" to status: ${newStatus}`); } catch (error) { console.error('[TaskManager] Error updating task status:', error); throw new Error(`Failed to update task status: ${error instanceof Error ? error.message : String(error)}`); } } /** * Suggest the next task to work on from a feature */ async suggestNextTask(featureIdentifier: string): Promise<string | null> { try { // Build the path to the feature task file const filePath = path.join(this.featureTasksDir, `feature_${featureIdentifier}_tasks.md`); // Check if file exists try { await fs.access(filePath); } catch (error) { throw new Error(`Task file for feature "${featureIdentifier}" not found.`); } // Read the file content const content = await fs.readFile(filePath, 'utf-8'); // Parse tasks from the content const tasks = this.parseTasks(content); // Find the first task with status 'ToDo' const nextTask = tasks.find(task => task.status === 'ToDo'); if (!nextTask) { console.log(`[TaskManager] No pending tasks found for feature "${featureIdentifier}".`); return null; } console.log(`[TaskManager] Suggested next task: "${nextTask.text}"`); return nextTask.text; } catch (error) { console.error('[TaskManager] Error suggesting next task:', error); throw new Error(`Failed to suggest next task: ${error instanceof Error ? error.message : String(error)}`); } } /** * Parse tasks from a markdown file content * This regex looks for GitHub-style task list items: * - [ ] Task description (ToDo) * - [x] Task description (Done) * - [X] Task description (Done, alternative) */ private parseTasks(content: string): Task[] { const tasks: Task[] = []; const lines = content.split('\n'); // Regular expression to match task checklist items const taskRegex = /^(\s*)-\s*\[([ xX])\]\s*(.+)$/; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = line.match(taskRegex); if (match) { const statusChar = match[2]; const status: TaskStatus = statusChar === ' ' ? 'ToDo' : statusChar.toLowerCase() === 'x' ? 'Done' : 'InProgress'; // Fallback, though we expect just [ ] and [x] tasks.push({ text: match[3].trim(), status, lineNumber: i, originalLine: line }); } } return tasks; } /** * Update a task's status in the content string */ private updateTaskInContent(content: string, task: Task, newStatus: TaskStatus): string { // Split content into lines const lines = content.split('\n'); // Get the original line const originalLine = lines[task.lineNumber]; // Replace the status marker let updatedLine: string; switch (newStatus) { case 'ToDo': updatedLine = originalLine.replace(/\[([ xX])\]/, '[ ]'); break; case 'InProgress': // Optional: you could use a different marker for in-progress, e.g., [~] // For now, we'll treat it the same as ToDo updatedLine = originalLine.replace(/\[([ xX])\]/, '[ ]'); break; case 'Done': updatedLine = originalLine.replace(/\[([ xX])\]/, '[x]'); break; default: updatedLine = originalLine; } // Replace the line in the content lines[task.lineNumber] = updatedLine; // Join the lines back into a string return lines.join('\n'); } }