const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const EventEmitter = require('events'); /** * Enhanced Claude Service with Full Development Workflow * Supports: Planning → Brainstorming → Coding → Live Preview */ class ClaudeWorkflowService extends EventEmitter { constructor(vaultPath) { super(); this.vaultPath = vaultPath; this.sessions = new Map(); } /** * Create a new session with workflow tracking */ createSession(options = {}) { const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const workingDir = options.workingDir || this.vaultPath; const session = { id: sessionId, workingDir, status: 'running', createdAt: new Date().toISOString(), lastActivity: new Date().toISOString(), outputBuffer: [], // Workflow stages workflow: { currentPhase: 'planning', // planning, brainstorming, coding, preview phases: { planning: { status: 'pending', output: [] }, brainstorming: { status: 'pending', output: [] }, coding: { status: 'pending', output: [], filesCreated: [] }, preview: { status: 'pending', url: null, processId: null } } }, // Tool usage tracking toolsUsed: [], metadata: options.metadata || {} }; this.sessions.set(sessionId, session); console.log(`[WorkflowService] Session ${sessionId} created`); return session; } /** * Send command with automatic workflow detection */ async sendCommand(sessionId, command) { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } console.log(`[WorkflowService] Processing command:`, command); // Detect phase based on command content const phase = this.detectPhase(command); session.workflow.currentPhase = phase; // Update phase status session.workflow.phases[phase].status = 'active'; // Execute command based on phase let result; switch (phase) { case 'planning': result = await this.executePlanningPhase(session, command); break; case 'brainstorming': result = await this.executeBrainstormingPhase(session, command); break; case 'coding': result = await this.executeCodingPhase(session, command); break; case 'preview': result = await this.executePreviewPhase(session, command); break; default: result = await this.executeDefaultPhase(session, command); } return result; } /** * Detect which phase the command belongs to */ detectPhase(command) { const lower = command.toLowerCase(); if (lower.includes('plan') || lower.includes('design') || lower.includes('architecture')) { return 'planning'; } if (lower.includes('brainstorm') || lower.includes('ideas') || lower.includes('approach')) { return 'brainstorming'; } if (lower.includes('create') || lower.includes('build') || lower.includes('implement') || lower.includes('write code') || lower.includes('make a')) { return 'coding'; } if (lower.includes('preview') || lower.includes('run') || lower.includes('start') || lower.includes('deploy') || lower.includes('test')) { return 'preview'; } return 'planning'; // default } /** * Execute Planning Phase - Show structured plan */ async executePlanningPhase(session, command) { console.log(`[WorkflowService] ===== PLANNING PHASE =====`); const claude = spawn('claude', ['-p', command], { cwd: session.workingDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: process.env.PATH }, shell: true }); let output = ''; let structuredPlan = null; claude.stdout.on('data', (data) => { const text = data.toString(); output += text; // Try to extract structured plan const planMatch = text.match(/## Plan\n([\s\S]*?)(?=\n##|$)/); if (planMatch) { structuredPlan = this.parsePlan(planMatch[1]); } this.emitPhaseUpdate(session, 'planning', text); }); await new Promise(resolve => claude.on('close', resolve)); session.workflow.phases.planning.status = 'completed'; session.workflow.phases.planning.output.push({ content: output, timestamp: new Date() }); return { phase: 'planning', output, structuredPlan }; } /** * Execute Brainstorming Phase - Explore alternatives */ async executeBrainstormingPhase(session, command) { console.log(`[WorkflowService] ===== BRAINSTORMING PHASE =====`); const brainstormingPrompt = ` Task: ${command} Please brainstorm 2-3 different approaches to solve this. For each approach: 1. Describe the approach briefly 2. List pros and cons 3. Recommend the best option with reasoning Format your response as: ## Approach 1: [Name] **Description**: ... **Pros**: ... **Cons**: ... ## Recommendation [Your recommended approach with reasoning] `; const claude = spawn('claude', ['-p', brainstormingPrompt], { cwd: session.workingDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: process.env.PATH }, shell: true }); let output = ''; let alternatives = []; claude.stdout.on('data', (data) => { const text = data.toString(); output += text; // Extract alternatives const approaches = text.match(/## Approach \d+: ([^\n]+)/g); if (approaches) { alternatives = approaches.map(a => a.replace('## Approach \\d+: ', '')); } this.emitPhaseUpdate(session, 'brainstorming', text); }); await new Promise(resolve => claude.on('close', resolve)); session.workflow.phases.brainstorming.status = 'completed'; session.workflow.phases.brainstorming.output.push({ content: output, timestamp: new Date() }); return { phase: 'brainstorming', output, alternatives }; } /** * Execute Coding Phase - Actually create files */ async executeCodingPhase(session, command) { console.log(`[WorkflowService] ===== CODING PHASE =====`); // First, get the plan from Claude const planningPrompt = ` Task: ${command} Please provide a detailed implementation plan including: 1. File structure (what files to create) 2. For each file, specify the exact content in a code block Format: ## File Structure - file1.ext - file2.ext ## Files to Create ### file1.ext \`\`\`language code here \`\`\` ### file2.ext \`\`\`language code here \`\`\` `; const claude = spawn('claude', ['-p', planningPrompt], { cwd: session.workingDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: process.env.PATH }, shell: true }); let output = ''; const filesToCreate = []; claude.stdout.on('data', (data) => { const text = data.toString(); output += text; // Extract files to create const fileMatches = text.match(/### ([^\n]+)\n```(\w+)?\n([\s\S]*?)```/g); if (fileMatches) { fileMatches.forEach(match => { const [, filename, lang, content] = match.match(/### ([^\n]+)\n```(\w+)?\n([\s\S]*?)```/); filesToCreate.push({ filename, content: content.trim() }); }); } this.emitPhaseUpdate(session, 'coding', text); }); await new Promise(resolve => claude.on('close', resolve)); // Actually create the files const createdFiles = []; for (const file of filesToCreate) { try { const filePath = path.join(session.workingDir, file.filename); const dir = path.dirname(filePath); // Create directory if needed if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write file fs.writeFileSync(filePath, file.content, 'utf-8'); createdFiles.push(file.filename); console.log(`[WorkflowService] Created file: ${file.filename}`); this.emit('file-created', { sessionId: session.id, filename: file.filename, path: filePath }); } catch (error) { console.error(`[WorkflowService] Failed to create ${file.filename}:`, error); } } session.workflow.phases.coding.status = 'completed'; session.workflow.phases.coding.output.push({ content: output, timestamp: new Date() }); session.workflow.phases.coding.filesCreated = createdFiles; return { phase: 'coding', output, filesCreated: createdFiles }; } /** * Execute Preview Phase - Run and preview the app */ async executePreviewPhase(session, command) { console.log(`[WorkflowService] ===== PREVIEW PHASE =====`); // Detect package.json to determine how to run const packageJsonPath = path.join(session.workingDir, 'package.json'); let startCommand = null; let port = null; if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); // Check for common scripts if (packageJson.scripts) { if (packageJson.scripts.dev) startCommand = 'npm run dev'; else if (packageJson.scripts.start) startCommand = 'npm start'; else if (packageJson.scripts.preview) startCommand = 'npm run preview'; } // Try to detect port if (packageJson.scripts?.dev?.includes('--port')) { const portMatch = packageJson.scripts.dev.match(/--port (\d+)/); if (portMatch) port = parseInt(portMatch[1]); } } if (!startCommand) { // Check for common files if (fs.existsSync(path.join(session.workingDir, 'index.html'))) { // Static HTML - use simple HTTP server startCommand = 'npx -y http-server -p 8080'; port = 8080; } else if (fs.existsSync(path.join(session.workingDir, 'vite.config.js'))) { startCommand = 'npm run dev -- --port 5173'; port = 5173; } } if (!startCommand) { return { phase: 'preview', error: 'Could not determine how to run this project. Please specify the start command.', suggestion: 'Try running: npm install && npm run dev' }; } console.log(`[WorkflowService] Starting: ${startCommand}`); // Start the dev server const serverProcess = spawn('sh', ['-c', startCommand], { cwd: session.workingDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: process.env.PATH }, detached: true }); let serverOutput = ''; serverProcess.stdout.on('data', (data) => { const text = data.toString(); serverOutput += text; // Try to extract port from output const portMatch = text.match(/(?:localhost|http:\/\/):(\d+)/); if (portMatch && !port) { port = parseInt(portMatch[1]); } this.emitPhaseUpdate(session, 'preview', `[Server] ${text}`); }); serverProcess.stderr.on('data', (data) => { const text = data.toString(); serverOutput += text; this.emitPhaseUpdate(session, 'preview', `[Server Error] ${text}`); }); // Wait a bit for server to start await new Promise(resolve => setTimeout(resolve, 3000)); // Unref to keep it running in background serverProcess.unref(); const previewUrl = port ? `http://localhost:${port}` : null; session.workflow.phases.preview.status = 'running'; session.workflow.phases.preview.url = previewUrl; session.workflow.phases.preview.processId = serverProcess.pid; session.workflow.phases.preview.output.push({ content: serverOutput, timestamp: new Date() }); this.emit('preview-ready', { sessionId: session.id, url: previewUrl, processId: serverProcess.pid }); return { phase: 'preview', command: startCommand, url: previewUrl, output: serverOutput }; } /** * Default phase execution */ async executeDefaultPhase(session, command) { const claude = spawn('claude', ['-p', command], { cwd: session.workingDir, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: process.env.PATH }, shell: true }); let output = ''; claude.stdout.on('data', (data) => { output += data.toString(); this.emit('session-output', { sessionId: session.id, content: data.toString() }); }); await new Promise(resolve => claude.on('close', resolve)); return { phase: 'default', output }; } /** * Parse structured plan from Claude output */ parsePlan(text) { const steps = []; const stepMatches = text.match(/\d+\.([^\n]+)/g); if (stepMatches) { stepMatches.forEach(match => { steps.push(match.replace(/^\d+\.\s*/, '')); }); } return { steps }; } /** * Emit phase update for real-time UI updates */ emitPhaseUpdate(session, phase, content) { this.emit('phase-update', { sessionId: session.id, phase, content, timestamp: new Date() }); // Also add to output buffer session.outputBuffer.push({ type: 'stdout', timestamp: new Date().toISOString(), content, phase }); session.lastActivity = new Date().toISOString(); } /** * Get session details */ getSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } return session; } /** * Stop preview server */ stopPreview(sessionId) { const session = this.sessions.get(sessionId); if (!session || !session.workflow.phases.preview.processId) { return { success: false, error: 'No preview server running' }; } try { process.kill(-session.workflow.phases.preview.processId, 'SIGTERM'); session.workflow.phases.preview.status = 'stopped'; session.workflow.phases.preview.processId = null; return { success: true }; } catch (error) { return { success: false, error: error.message }; } } } module.exports = ClaudeWorkflowService;