lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
191 lines (156 loc) • 5.99 kB
text/typescript
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');
}
}