Files
SuperCharged-Claude-Code-Up…/services/claude-workflow-service.js
uroma 0dd2083556 Initial commit: Obsidian Web Interface for Claude Code
- Full IDE with terminal integration using xterm.js
- Session management with local and web sessions
- HTML preview functionality
- Multi-terminal support with session picker

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:29:44 +00:00

516 lines
14 KiB
JavaScript

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;