lamplighter-mcp
Version:
An intelligent context engine for AI-assisted software development
225 lines (183 loc) • 8.01 kB
text/typescript
import * as fs from 'fs/promises';
import * as path from 'path';
import dotenv from 'dotenv';
import { AIService } from '../services/aiService';
// Load environment variables
dotenv.config();
const DEFAULT_CONTEXT_DIR = './lamplighter_context';
const FEATURE_TASKS_DIR = 'feature_tasks';
export class FeatureSpecProcessor {
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);
// Ensure the feature tasks directory exists
this.ensureDirectories();
console.log(`[FeatureSpecProcessor] Initialized. Task files will be saved to: ${this.featureTasksDir}`);
}
/**
* Create necessary directories if they don't exist
*/
private async ensureDirectories(): Promise<void> {
try {
await fs.mkdir(this.contextDir, { recursive: true });
await fs.mkdir(this.featureTasksDir, { recursive: true });
} catch (error) {
console.error('[FeatureSpecProcessor] Error creating directories:', error);
throw new Error(`Failed to create required directories: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Derive a feature ID from a URL or title
*/
deriveFeatureId(urlOrTitle: string): string {
let featureId = '';
try {
// Try to extract a feature ID from a URL
const url = new URL(urlOrTitle);
// Get the last part of the path (usually the page title in Confluence URLs)
const pathParts = url.pathname.split('/').filter(Boolean);
if (pathParts.length > 0) {
featureId = pathParts[pathParts.length - 1];
} else {
// If no path parts, use the hostname
featureId = url.hostname.split('.')[0];
}
// Decode URL encoding like %20 or +
featureId = decodeURIComponent(featureId.replace(/\+/g, ' '));
} catch (error) {
// If not a valid URL, use the string as is
featureId = urlOrTitle;
}
// Sanitize: remove special characters, replace spaces/hyphens with underscores
featureId = featureId
.trim()
.replace(/\s+/g, '_') // Replace spaces (and now decoded +/ %20) with underscores
.replace(/-+/g, '_') // Replace hyphens with underscores
.replace(/[^a-z0-9_]/gi, '') // Remove remaining non-alphanumeric chars except underscore
.replace(/_+/g, '_') // Collapse multiple underscores
.replace(/^_+|_+$/g, '') // Trim leading/trailing underscores
.toLowerCase(); // Convert to lowercase
// Ensure we have something valid
if (!featureId) {
// Generate a timestamp-based ID as fallback
featureId = `feature_${Date.now()}`;
}
return featureId;
}
/**
* Process a specification into tasks
*/
async processSpecification(
specText: string,
codebaseSummary: string,
featureIdentifier: string
): Promise<string> {
console.log(`[FeatureSpecProcessor] Processing specification for feature: ${featureIdentifier}`);
try {
// Construct prompt for the LLM
const prompt = this.constructPrompt(specText, codebaseSummary);
// Generate task list using AI
console.log('[FeatureSpecProcessor] Generating task list with AI...');
const rawTaskListMarkdown = await AIService.generateText(prompt, { temperature: 0.3 });
// Validate and clean the AI response
const validatedTaskListMarkdown = this.validateAndCleanTaskList(rawTaskListMarkdown);
// Format the final markdown content
const finalMarkdown = this.formatFinalMarkdown(specText, validatedTaskListMarkdown, featureIdentifier);
// Write to file
const filePath = path.join(this.featureTasksDir, `feature_${featureIdentifier}_tasks.md`);
await fs.writeFile(filePath, finalMarkdown, 'utf-8');
console.log(`[FeatureSpecProcessor] Task list saved to: ${filePath}`);
return filePath;
} catch (error) {
console.error('[FeatureSpecProcessor] Error processing specification:', error);
throw new Error(`Failed to process feature specification: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Validate and clean the raw markdown string from the AI to ensure it's a checklist.
*/
private validateAndCleanTaskList(rawMarkdown: string): string {
if (!rawMarkdown || typeof rawMarkdown !== 'string') {
throw new Error('AI response was empty or invalid.');
}
const lines = rawMarkdown.split('\n');
const validLines: string[] = [];
const taskRegex = /^- \[ \] .+$/; // Matches "- [ ] Task description"
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '') {
continue; // Skip empty lines
}
if (!taskRegex.test(trimmedLine)) {
console.error(`[FeatureSpecProcessor] Invalid task list format from AI. Line: "${trimmedLine}"`);
throw new Error('AI did not return a valid Markdown task checklist. Please check the AI response or prompt.');
}
validLines.push(trimmedLine); // Keep the trimmed, valid line
}
if (validLines.length === 0) {
throw new Error('AI response did not contain any valid task list items.');
}
return validLines.join('\n');
}
/**
* Construct the prompt for the AI
*/
private constructPrompt(specText: string, codebaseSummary: string): string {
return `
You are a software development task analyzer. Your job is to break down a feature specification into
a well-structured list of actionable development tasks.
## Codebase Context
The following is a summary of the codebase structure. Use this information to reference specific modules,
components, or patterns when creating tasks:
${codebaseSummary}
## Feature Specification
${specText}
## Instructions
1. Analyze the feature specification.
2. Break it down into a logical sequence of development tasks.
3. Format the tasks as a Markdown checklist using GitHub-style "- [ ] Task description" syntax.
4. Ensure tasks are specific, actionable, and sized appropriately (not too broad or too narrow).
5. Reference relevant modules, patterns, or components from the codebase summary when appropriate.
6. Include tasks for tests if testing is implied by the feature.
7. Order tasks in a logical implementation sequence.
8. DO NOT include any explanatory text before or after the task list - ONLY output the task checklist.
Example output format:
- [ ] Initialize the XYZ module in the authentication system
- [ ] Implement the database schema changes for user preferences
- [ ] Create API endpoints in the UserController
Now, please create a task checklist for implementing the feature:
`;
}
/**
* Format the final markdown document
*/
private formatFinalMarkdown(specText: string, taskListMarkdown: string, featureId: string): string {
// Extract a title from the first few lines of the spec
const specLines = specText.split('\n');
const title = specLines[0]?.trim() || `Feature: ${featureId}`;
// Create a timestamp
const timestamp = new Date().toISOString();
// Format the complete markdown document
return `# ${title}
## Feature Specification Summary
*Generated at: ${timestamp}*
${this.createSpecSummary(specText)}
## Implementation Tasks
${taskListMarkdown}
---
*This task list was automatically generated from the feature specification by Lamplighter-MCP.*
`;
}
/**
* Create a summary from the spec text
*/
private createSpecSummary(specText: string): string {
// For simplicity, take the first few lines of the spec
// In a more advanced implementation, you could use the AI to generate a summary
const specLines = specText.split('\n');
const summary = specLines.slice(0, 5).join('\n');
return summary + (specLines.length > 5 ? '\n...' : '');
}
}