Replaces WebContainer-based approach with simpler Claude Code CLI session shell command execution. This eliminates COOP/COEP header requirements and reduces bundle size by ~350KB. Changes: - Added executeShellCommand() to ClaudeService for spawning bash processes - Added /claude/api/shell-command API endpoint for executing commands - Updated Full Stack mode (chat-functions.js) to use CLI sessions - Simplified terminal mode by removing WebContainer dependencies Benefits: - No SharedArrayBuffer/COOP/COEP issues - Uses existing Claude Code infrastructure - Faster startup, more reliable execution - Better error handling and output capture Fixes: - Terminal creation failure in Full Stack mode - WebContainer SharedArrayBuffer serialization errors Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
769 lines
22 KiB
JavaScript
769 lines
22 KiB
JavaScript
const { spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const EventEmitter = require('events');
|
|
const os = require('os');
|
|
const { SYSTEM_PROMPT } = require('./system-prompt');
|
|
const { extractAllTags, generateOperationSummary, getDyadWriteTags } = require('./tag-parser');
|
|
const ResponseProcessor = require('./response-processor');
|
|
|
|
class ClaudeCodeService extends EventEmitter {
|
|
constructor(vaultPath) {
|
|
super();
|
|
this.vaultPath = vaultPath;
|
|
this.sessions = new Map();
|
|
this.claudeSessionsDir = path.join(vaultPath, 'Claude Sessions');
|
|
this.responseProcessor = new ResponseProcessor(vaultPath);
|
|
this.ensureDirectories();
|
|
}
|
|
|
|
ensureDirectories() {
|
|
if (!fs.existsSync(this.claudeSessionsDir)) {
|
|
fs.mkdirSync(this.claudeSessionsDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new Claude Code session
|
|
*/
|
|
createSession(options = {}) {
|
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
const workingDir = options.workingDir || this.vaultPath;
|
|
|
|
console.log(`[ClaudeService] Creating session ${sessionId} in ${workingDir}`);
|
|
|
|
const session = {
|
|
id: sessionId,
|
|
pid: null,
|
|
process: null,
|
|
workingDir,
|
|
status: 'running',
|
|
createdAt: new Date().toISOString(),
|
|
lastActivity: new Date().toISOString(),
|
|
outputBuffer: [],
|
|
context: {
|
|
messages: [],
|
|
totalTokens: 0,
|
|
maxTokens: 200000
|
|
},
|
|
metadata: options.metadata || {}
|
|
};
|
|
|
|
// Add to sessions map
|
|
this.sessions.set(sessionId, session);
|
|
console.log(`[ClaudeService] Session ${sessionId} created successfully (using -p mode)`);
|
|
|
|
// Save session initialization
|
|
this.saveSessionToVault(session);
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Execute shell command in session (for Full Stack mode)
|
|
* Spawns a shell process, sends command, captures output
|
|
*/
|
|
async executeShellCommand(sessionId, command) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
if (session.status !== 'running') {
|
|
throw new Error(`Session ${sessionId} is not running`);
|
|
}
|
|
|
|
console.log(`[ClaudeService] Executing shell command in ${sessionId}:`, command);
|
|
|
|
// Spawn shell to execute the command
|
|
const shell = spawn('bash', ['-c', command], {
|
|
cwd: session.workingDir,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
env: {
|
|
...process.env,
|
|
TERM: 'xterm-256color'
|
|
}
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
shell.stdout.on('data', (data) => {
|
|
const text = data.toString();
|
|
stdout += text;
|
|
|
|
// Add to output buffer
|
|
session.outputBuffer.push({
|
|
type: 'shell',
|
|
timestamp: new Date().toISOString(),
|
|
content: text
|
|
});
|
|
|
|
// Emit for real-time updates
|
|
this.emit('session-output', {
|
|
sessionId,
|
|
type: 'stdout',
|
|
content: text
|
|
});
|
|
});
|
|
|
|
shell.stderr.on('data', (data) => {
|
|
const text = data.toString();
|
|
stderr += text;
|
|
|
|
session.outputBuffer.push({
|
|
type: 'stderr',
|
|
timestamp: new Date().toISOString(),
|
|
content: text
|
|
});
|
|
|
|
this.emit('session-output', {
|
|
sessionId,
|
|
type: 'stderr',
|
|
content: text
|
|
});
|
|
});
|
|
|
|
return new Promise((resolve) => {
|
|
shell.on('close', (code) => {
|
|
const exitCode = code !== null ? code : -1;
|
|
|
|
console.log(`[ClaudeService] Shell command completed with exit code ${exitCode}`);
|
|
|
|
resolve({
|
|
exitCode,
|
|
stdout,
|
|
stderr,
|
|
success: exitCode === 0
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Send command to a session using -p (print) mode
|
|
*/
|
|
sendCommand(sessionId, command) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
// Check if it's a historical session
|
|
try {
|
|
const historicalSessions = this.loadHistoricalSessions();
|
|
const isHistorical = historicalSessions.some(s => s.id === sessionId);
|
|
|
|
if (isHistorical) {
|
|
throw new Error(`Session ${sessionId} is a historical session and cannot accept new commands. Please start a new chat session.`);
|
|
}
|
|
} catch (error) {
|
|
// Ignore error from checking historical sessions
|
|
}
|
|
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
if (session.status !== 'running') {
|
|
throw new Error(`Session ${sessionId} is not running (status: ${session.status})`);
|
|
}
|
|
|
|
console.log(`[ClaudeService] Sending command to session ${sessionId}:`, command);
|
|
|
|
// Track command in context
|
|
session.context.messages.push({
|
|
role: 'user',
|
|
content: command,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Also save user message to outputBuffer for persistence
|
|
session.outputBuffer.push({
|
|
type: 'user',
|
|
role: 'user',
|
|
timestamp: new Date().toISOString(),
|
|
content: command
|
|
});
|
|
|
|
session.lastActivity = new Date().toISOString();
|
|
|
|
this.emit('command-sent', {
|
|
sessionId,
|
|
command
|
|
});
|
|
|
|
// Prepend system prompt to command for tag-based output
|
|
const fullCommand = `${SYSTEM_PROMPT}\n\n${command}`;
|
|
|
|
// Spawn claude in -p (print) mode for this command
|
|
const claude = spawn('claude', ['-p', fullCommand], {
|
|
cwd: session.workingDir,
|
|
stdio: ['ignore', 'pipe', 'pipe'], // Explicitly set stdio to get stdout/stderr
|
|
env: {
|
|
...process.env,
|
|
TERM: 'xterm-256color'
|
|
}
|
|
});
|
|
|
|
let output = '';
|
|
let stderrOutput = '';
|
|
|
|
claude.stdout.on('data', (data) => {
|
|
const text = data.toString();
|
|
console.log(`[ClaudeService] [${sessionId}] stdout:`, text.substring(0, 100));
|
|
output += text;
|
|
|
|
// Add to output buffer
|
|
session.outputBuffer.push({
|
|
type: 'stdout',
|
|
timestamp: new Date().toISOString(),
|
|
content: text
|
|
});
|
|
|
|
// Emit for real-time updates
|
|
this.emit('session-output', {
|
|
sessionId,
|
|
type: 'stdout',
|
|
content: text
|
|
});
|
|
});
|
|
|
|
claude.stderr.on('data', (data) => {
|
|
const text = data.toString();
|
|
console.error(`[ClaudeService] [${sessionId}] stderr:`, text.substring(0, 100));
|
|
stderrOutput += text;
|
|
|
|
session.outputBuffer.push({
|
|
type: 'stderr',
|
|
timestamp: new Date().toISOString(),
|
|
content: text
|
|
});
|
|
|
|
this.emit('session-output', {
|
|
sessionId,
|
|
type: 'stderr',
|
|
content: text
|
|
});
|
|
});
|
|
|
|
claude.on('close', (code) => {
|
|
console.log(`[ClaudeService] [${sessionId}] Command completed with exit code ${code}`);
|
|
|
|
// Add assistant response to context
|
|
if (output.trim()) {
|
|
session.context.messages.push({
|
|
role: 'assistant',
|
|
content: output,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Parse tags from output
|
|
const tags = extractAllTags(output);
|
|
if (tags.writes.length > 0 || tags.renames.length > 0 || tags.deletes.length > 0 || tags.dependencies.length > 0) {
|
|
const operations = generateOperationSummary(tags);
|
|
|
|
this.emit('operations-detected', {
|
|
sessionId,
|
|
response: output,
|
|
tags,
|
|
operations
|
|
});
|
|
|
|
console.log(`[ClaudeService] Detected ${operations.length} operations requiring approval`);
|
|
}
|
|
|
|
this.emit('command-complete', {
|
|
sessionId,
|
|
exitCode: code,
|
|
output
|
|
});
|
|
|
|
// Save session to vault
|
|
this.saveSessionToVault(session);
|
|
});
|
|
|
|
claude.on('error', (error) => {
|
|
console.error(`[ClaudeService] [${sessionId}] Process error:`, error);
|
|
this.emit('session-error', {
|
|
sessionId,
|
|
error: error.message
|
|
});
|
|
});
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* Get session details (handles both active and historical sessions)
|
|
*/
|
|
getSession(sessionId) {
|
|
// First check active sessions
|
|
let session = this.sessions.get(sessionId);
|
|
|
|
if (session) {
|
|
return {
|
|
id: session.id,
|
|
pid: session.pid,
|
|
workingDir: session.workingDir,
|
|
status: session.status,
|
|
createdAt: session.createdAt,
|
|
lastActivity: session.lastActivity,
|
|
terminatedAt: session.terminatedAt,
|
|
exitCode: session.exitCode,
|
|
outputBuffer: session.outputBuffer,
|
|
context: session.context,
|
|
metadata: session.metadata
|
|
};
|
|
}
|
|
|
|
// If not found in active sessions, check historical sessions
|
|
try {
|
|
const historicalSessions = this.loadHistoricalSessions();
|
|
const historical = historicalSessions.find(s => s.id === sessionId);
|
|
|
|
if (historical) {
|
|
// Load the full session from file
|
|
let sessionFiles = [];
|
|
try {
|
|
sessionFiles = fs.readdirSync(this.claudeSessionsDir);
|
|
} catch (err) {
|
|
console.error('Cannot read sessions directory:', err);
|
|
throw new Error('Sessions directory not accessible');
|
|
}
|
|
|
|
const sessionFile = sessionFiles.find(f => f.includes(sessionId));
|
|
|
|
if (sessionFile) {
|
|
const filepath = path.join(this.claudeSessionsDir, sessionFile);
|
|
|
|
// Check if file exists
|
|
if (!fs.existsSync(filepath)) {
|
|
console.error('Session file does not exist:', filepath);
|
|
throw new Error('Session file not found');
|
|
}
|
|
|
|
const content = fs.readFileSync(filepath, 'utf-8');
|
|
|
|
// Parse output buffer from markdown
|
|
const outputBuffer = this.parseOutputFromMarkdown(content);
|
|
|
|
return {
|
|
id: historical.id,
|
|
pid: null,
|
|
workingDir: historical.workingDir,
|
|
status: historical.status,
|
|
createdAt: historical.created_at,
|
|
lastActivity: historical.created_at,
|
|
terminatedAt: historical.created_at,
|
|
exitCode: null,
|
|
outputBuffer,
|
|
context: {
|
|
messages: [],
|
|
totalTokens: 0,
|
|
maxTokens: 200000
|
|
},
|
|
metadata: {
|
|
project: historical.project,
|
|
historical: true
|
|
}
|
|
};
|
|
} else {
|
|
throw new Error('Session file not found in directory');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading historical session:', error);
|
|
// Re-throw with more context
|
|
if (error.message.includes('not found')) {
|
|
throw error;
|
|
}
|
|
throw new Error(`Failed to load historical session: ${error.message}`);
|
|
}
|
|
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
/**
|
|
* Parse output buffer from session markdown file
|
|
*/
|
|
parseOutputFromMarkdown(markdown) {
|
|
const outputBuffer = [];
|
|
const lines = markdown.split('\n');
|
|
let currentSection = null;
|
|
let currentContent = [];
|
|
let currentTimestamp = null;
|
|
|
|
for (const line of lines) {
|
|
// Check for output sections
|
|
if (line.match(/^### \w+ - (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/)) {
|
|
// Save previous section if exists
|
|
if (currentSection && currentContent.length > 0) {
|
|
outputBuffer.push({
|
|
type: currentSection,
|
|
timestamp: currentTimestamp,
|
|
content: currentContent.join('\n')
|
|
});
|
|
}
|
|
|
|
// Start new section
|
|
const match = line.match(/^### (\w+) - (.+)$/);
|
|
if (match) {
|
|
currentSection = match[1].toLowerCase();
|
|
currentTimestamp = match[2];
|
|
currentContent = [];
|
|
}
|
|
} else if (currentSection && line.match(/^```$/)) {
|
|
// End of code block
|
|
if (currentContent.length > 0) {
|
|
outputBuffer.push({
|
|
type: currentSection,
|
|
timestamp: currentTimestamp,
|
|
content: currentContent.join('\n')
|
|
});
|
|
}
|
|
currentSection = null;
|
|
currentContent = [];
|
|
} else if (currentSection) {
|
|
currentContent.push(line);
|
|
}
|
|
}
|
|
|
|
// Don't forget the last section
|
|
if (currentSection && currentContent.length > 0) {
|
|
outputBuffer.push({
|
|
type: currentSection,
|
|
timestamp: currentTimestamp,
|
|
content: currentContent.join('\n')
|
|
});
|
|
}
|
|
|
|
return outputBuffer;
|
|
}
|
|
|
|
/**
|
|
* List all sessions
|
|
*/
|
|
listSessions() {
|
|
return Array.from(this.sessions.values()).map(session => {
|
|
const metadata = this.calculateSessionMetadata(session);
|
|
return {
|
|
id: session.id,
|
|
pid: session.pid,
|
|
workingDir: session.workingDir,
|
|
status: session.status,
|
|
createdAt: session.createdAt,
|
|
lastActivity: session.lastActivity,
|
|
metadata: session.metadata,
|
|
...metadata
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate enhanced session metadata
|
|
*/
|
|
calculateSessionMetadata(session) {
|
|
const metadata = {
|
|
lastMessage: null,
|
|
fileCount: 0,
|
|
messageCount: 0
|
|
};
|
|
|
|
if (session.outputBuffer && session.outputBuffer.length > 0) {
|
|
// Get last message
|
|
const lastEntry = session.outputBuffer[session.outputBuffer.length - 1];
|
|
metadata.lastMessage = this.extractMessagePreview(lastEntry.content);
|
|
|
|
// Count dyad-write tags (files created/modified)
|
|
metadata.fileCount = session.outputBuffer.reduce((count, entry) => {
|
|
const writeTags = getDyadWriteTags(entry.content);
|
|
return count + writeTags.length;
|
|
}, 0);
|
|
|
|
metadata.messageCount = session.outputBuffer.length;
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
/**
|
|
* Extract message preview (first 100 chars, stripped of tags)
|
|
*/
|
|
extractMessagePreview(content) {
|
|
if (!content) {
|
|
return 'No messages yet';
|
|
}
|
|
|
|
// Remove dyad tags and strip markdown code blocks (chained for efficiency)
|
|
let preview = content
|
|
.replace(/<dyad-write[^>]*>[\s\S]*?<\/dyad-write>/g, '[File]')
|
|
.replace(/<dyad-[a-z-]+(?:\s+[^>]*)?>/g, '')
|
|
.replace(/```[\s\S]*?```/g, '[Code]');
|
|
|
|
// Get first 100 chars
|
|
preview = preview.substring(0, 100);
|
|
|
|
// Truncate at last word boundary
|
|
if (preview.length === 100) {
|
|
const lastSpace = preview.lastIndexOf(' ');
|
|
if (lastSpace > 50) {
|
|
preview = preview.substring(0, lastSpace);
|
|
} else {
|
|
// If no good word boundary, truncate at a safe point
|
|
preview = preview.substring(0, 97);
|
|
}
|
|
preview += '...';
|
|
}
|
|
|
|
return preview.trim() || 'No messages yet';
|
|
}
|
|
|
|
/**
|
|
* Terminate a session
|
|
*/
|
|
terminateSession(sessionId) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
if (session.status !== 'running') {
|
|
throw new Error(`Session ${sessionId} is not running`);
|
|
}
|
|
|
|
session.process.kill();
|
|
session.status = 'terminating';
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* Parse context updates from output
|
|
*/
|
|
parseContextUpdate(session, output) {
|
|
// Look for token usage patterns
|
|
const tokenMatch = output.match(/(\d+) tokens? used/i);
|
|
if (tokenMatch) {
|
|
const tokens = parseInt(tokenMatch[1]);
|
|
session.context.totalTokens = tokens;
|
|
}
|
|
|
|
// Look for assistant responses
|
|
if (output.includes('Claude:') || output.includes('Assistant:')) {
|
|
session.context.messages.push({
|
|
role: 'assistant',
|
|
content: output,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save session to Obsidian vault
|
|
*/
|
|
saveSessionToVault(session) {
|
|
const date = new Date().toISOString().split('T')[0];
|
|
const filename = `${date}-${session.id}.md`;
|
|
const filepath = path.join(this.claudeSessionsDir, filename);
|
|
|
|
const content = this.generateSessionMarkdown(session);
|
|
|
|
try {
|
|
fs.writeFileSync(filepath, content, 'utf-8');
|
|
} catch (error) {
|
|
console.error('Failed to save session to vault:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate markdown representation of session
|
|
*/
|
|
generateSessionMarkdown(session) {
|
|
const lines = [];
|
|
|
|
lines.push('---');
|
|
lines.push(`type: claude-session`);
|
|
lines.push(`session_id: ${session.id}`);
|
|
lines.push(`status: ${session.status}`);
|
|
lines.push(`created_at: ${session.createdAt}`);
|
|
lines.push(`working_dir: ${session.workingDir}`);
|
|
if (session.metadata.project) {
|
|
lines.push(`project: ${session.metadata.project}`);
|
|
}
|
|
lines.push('---');
|
|
lines.push('');
|
|
lines.push(`# Claude Code Session: ${session.id}`);
|
|
lines.push('');
|
|
lines.push(`**Created**: ${session.createdAt}`);
|
|
lines.push(`**Status**: ${session.status}`);
|
|
lines.push(`**Working Directory**: \`${session.workingDir}\``);
|
|
if (session.pid) {
|
|
lines.push(`**PID**: ${session.pid}`);
|
|
}
|
|
lines.push('');
|
|
|
|
// Context summary
|
|
lines.push('## Context Usage');
|
|
lines.push('');
|
|
lines.push(`- **Total Tokens**: ${session.context.totalTokens}`);
|
|
lines.push(`- **Messages**: ${session.context.messages.length}`);
|
|
lines.push(`- **Token Limit**: ${session.context.maxTokens}`);
|
|
lines.push('');
|
|
|
|
// Output
|
|
lines.push('## Session Output');
|
|
lines.push('');
|
|
|
|
session.outputBuffer.forEach(entry => {
|
|
lines.push(`### ${entry.type} - ${entry.timestamp}`);
|
|
lines.push('');
|
|
lines.push('```');
|
|
lines.push(entry.content);
|
|
lines.push('```');
|
|
lines.push('');
|
|
});
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Load historical sessions from vault
|
|
*/
|
|
loadHistoricalSessions() {
|
|
const sessions = [];
|
|
|
|
try {
|
|
const files = fs.readdirSync(this.claudeSessionsDir);
|
|
const sessionFiles = files.filter(f => f.endsWith('.md') && f.includes('session-'));
|
|
|
|
sessionFiles.forEach(file => {
|
|
const filepath = path.join(this.claudeSessionsDir, file);
|
|
const content = fs.readFileSync(filepath, 'utf-8');
|
|
|
|
// Parse frontmatter
|
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
if (frontmatterMatch) {
|
|
const frontmatter = {};
|
|
frontmatterMatch[1].split('\n').forEach(line => {
|
|
const [key, ...valueParts] = line.split(':');
|
|
if (key && valueParts.length > 0) {
|
|
frontmatter[key.trim()] = valueParts.join(':').trim();
|
|
}
|
|
});
|
|
|
|
// Parse output buffer from content
|
|
const outputBuffer = this.parseOutputFromMarkdown(content);
|
|
|
|
// Calculate metadata
|
|
const tempSession = { outputBuffer };
|
|
const metadata = this.calculateSessionMetadata(tempSession);
|
|
|
|
sessions.push({
|
|
id: frontmatter.session_id,
|
|
status: frontmatter.status,
|
|
createdAt: frontmatter.created_at,
|
|
workingDir: frontmatter.working_dir,
|
|
project: frontmatter.project,
|
|
file: filepath,
|
|
...metadata
|
|
});
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load historical sessions:', error);
|
|
}
|
|
|
|
return sessions;
|
|
}
|
|
|
|
/**
|
|
* Get context statistics
|
|
*/
|
|
getContextStats(sessionId) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
return {
|
|
totalTokens: session.context.totalTokens,
|
|
maxTokens: session.context.maxTokens,
|
|
usagePercentage: (session.context.totalTokens / session.context.maxTokens) * 100,
|
|
messageCount: session.context.messages.length,
|
|
messages: session.context.messages
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clean up terminated sessions
|
|
*/
|
|
cleanup() {
|
|
const now = Date.now();
|
|
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
for (const [sessionId, session] of this.sessions.entries()) {
|
|
const sessionAge = now - new Date(session.createdAt).getTime();
|
|
|
|
if (session.status === 'terminated' && sessionAge > maxAge) {
|
|
this.sessions.delete(sessionId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute operations after user approval
|
|
*/
|
|
async executeOperations(sessionId, response, onProgress) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
console.log(`[ClaudeService] Executing operations for session ${sessionId}`);
|
|
|
|
try {
|
|
const results = await this.responseProcessor.processResponse(
|
|
sessionId,
|
|
response,
|
|
{
|
|
workingDir: session.workingDir,
|
|
autoApprove: true,
|
|
onProgress
|
|
}
|
|
);
|
|
|
|
this.emit('operations-executed', {
|
|
sessionId,
|
|
results
|
|
});
|
|
|
|
return results;
|
|
} catch (error) {
|
|
console.error(`[ClaudeService] Error executing operations:`, error);
|
|
this.emit('operations-error', {
|
|
sessionId,
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preview operations without executing
|
|
*/
|
|
async previewOperations(sessionId, response) {
|
|
const session = this.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
throw new Error(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
return await this.responseProcessor.previewOperations(response, session.workingDir);
|
|
}
|
|
}
|
|
|
|
module.exports = ClaudeCodeService;
|