const { XMLParser } = require('fast-xml-parser'); const fs = require('fs'); const path = require('path'); /** * Custom XML Tag Parser for Claude Code responses * Handles: , , , */ class ClaudeXMLParser { constructor(options = {}) { this.options = { ignoreAttributes: false, attributeNamePrefix: '', textNodeName: '#text', ...options }; this.parser = new XMLParser(this.options); } /** * Parse Claude's response and extract all custom tags */ parseResponse(response) { const result = { writes: [], edits: [], commands: [], dependencies: [], previews: [], text: response }; // Extract tags result.writes = this.extractClaudeWriteTags(response); // Extract tags result.edits = this.extractClaudeEditTags(response); // Extract tags result.commands = this.extractClaudeCommandTags(response); // Extract tags result.dependencies = this.extractClaudeDependencyTags(response); // Extract tags result.previews = this.extractClaudePreviewTags(response); // Clean text (remove tags for display) result.text = this.cleanText(response); return result; } /** * Extract tags * Format: content */ extractClaudeWriteTags(response) { const tags = []; const regex = /([\s\S]*?)<\/claude-write>/g; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'write', path: match[1], content: match[2].trim() }); } return tags; } /** * Extract tags * Format: * pattern * replacement * */ extractClaudeEditTags(response) { const tags = []; const regex = /([\s\S]*?)<\/claude-edit>/g; let match; while ((match = regex.exec(response)) !== null) { const content = match[3]; const searchMatch = content.match(/\s*([\s\S]*?)\s*<\/search>/); const replaceMatch = content.match(/\s*([\s\S]*?)\s*<\/replace>/); tags.push({ type: 'edit', path: match[1], mode: match[2] || 'replace', search: searchMatch ? searchMatch[1].trim() : '', replace: replaceMatch ? replaceMatch[1].trim() : '' }); } return tags; } /** * Extract tags * Format: command */ extractClaudeCommandTags(response) { const tags = []; const regex = /([\s\S]*?)<\/claude-command>/g; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'command', workingDir: match[1] || process.cwd(), command: match[2].trim() }); } return tags; } /** * Extract tags * Format: install-command */ extractClaudeDependencyTags(response) { const tags = []; const regex = /([\s\S]*?)<\/claude-dependency>/g; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'dependency', package: match[1], command: match[2].trim() }); } return tags; } /** * Extract tags * Format: */ extractClaudePreviewTags(response) { const tags = []; const regex = //g; let match; while ((match = regex.exec(response)) !== null) { tags.push({ type: 'preview', url: match[1] || `http://localhost:${match[2]}`, port: match[2] }); } return tags; } /** * Remove XML tags from response for clean text display */ cleanText(response) { return response .replace(/]*>[\s\S]*?<\/claude-write>/g, '[Wrote file]') .replace(/]*>[\s\S]*?<\/claude-edit>/g, '[Edited file]') .replace(/]*>[\s\S]*?<\/claude-command>/g, '[Executed command]') .replace(/]*>[\s\S]*?<\/claude-dependency>/g, '[Added dependency]') .replace(/]*>/g, '[Updated preview]') .trim(); } /** * Validate tag structure */ validateTag(tag) { const errors = []; if (tag.type === 'write') { if (!tag.path) errors.push('Missing path attribute'); if (tag.content === undefined) errors.push('Missing content'); } if (tag.type === 'edit') { if (!tag.path) errors.push('Missing path attribute'); if (!tag.search) errors.push('Missing search pattern'); if (!tag.replace) errors.push('Missing replacement'); } if (tag.type === 'command') { if (!tag.command) errors.push('Missing command'); } return { valid: errors.length === 0, errors }; } } /** * Response Processor - Executes parsed XML tags */ class ClaudeResponseProcessor { constructor(vaultPath) { this.vaultPath = vaultPath; this.parser = new ClaudeXMLParser(); this.executedOperations = []; } /** * Process Claude's response and execute all operations */ async process(response, options = {}) { const { dryRun = false, basePath = this.vaultPath } = options; const parsed = this.parser.parseResponse(response); const results = { writes: [], edits: [], commands: [], dependencies: [], previews: [], errors: [], summary: {} }; // Execute write operations for (const writeOp of parsed.writes) { try { const validation = this.parser.validateTag(writeOp); if (!validation.valid) { results.errors.push({ operation: 'write', errors: validation.errors }); continue; } if (!dryRun) { await this.executeWrite(writeOp, basePath); } results.writes.push({ path: writeOp.path, size: writeOp.content.length, success: true }); } catch (error) { results.errors.push({ operation: 'write', path: writeOp.path, error: error.message }); } } // Execute edit operations for (const editOp of parsed.edits) { try { const validation = this.parser.validateTag(editOp); if (!validation.valid) { results.errors.push({ operation: 'edit', errors: validation.errors }); continue; } if (!dryRun) { await this.executeEdit(editOp, basePath); } results.edits.push({ path: editOp.path, mode: editOp.mode, success: true }); } catch (error) { results.errors.push({ operation: 'edit', path: editOp.path, error: error.message }); } } // Execute command operations for (const cmdOp of parsed.commands) { try { const validation = this.parser.validateTag(cmdOp); if (!validation.valid) { results.errors.push({ operation: 'command', errors: validation.errors }); continue; } results.commands.push({ command: cmdOp.command, workingDir: cmdOp.workingDir, success: true, output: null // Will be filled when executed }); if (!dryRun) { const output = await this.executeCommand(cmdOp); results.commands[results.commands.length - 1].output = output; } } catch (error) { results.errors.push({ operation: 'command', command: cmdOp.command, error: error.message }); } } // Collect dependency operations for (const depOp of parsed.dependencies) { results.dependencies.push({ package: depOp.package, command: depOp.command }); } // Collect preview operations for (const previewOp of parsed.previews) { results.previews.push({ url: previewOp.url, port: previewOp.port }); } // Generate summary results.summary = { totalOperations: results.writes.length + results.edits.length + results.commands.length + results.dependencies.length, successful: results.writes.length + results.edits.length + results.commands.filter(c => c.success).length, failed: results.errors.length, filesWritten: results.writes.length, filesEdited: results.edits.length, commandsExecuted: results.commands.length }; return { parsed, results, cleanText: parsed.text }; } /** * Execute write operation */ async executeWrite(writeOp, basePath) { const fullPath = path.resolve(basePath, writeOp.path); // Create directory if it doesn't exist const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write file fs.writeFileSync(fullPath, writeOp.content, 'utf-8'); this.executedOperations.push({ type: 'write', path: fullPath, timestamp: new Date().toISOString() }); } /** * Execute edit operation */ async executeEdit(editOp, basePath) { const fullPath = path.resolve(basePath, editOp.path); if (!fs.existsSync(fullPath)) { throw new Error(`File not found: ${editOp.path}`); } let content = fs.readFileSync(fullPath, 'utf-8'); if (editOp.mode === 'replace') { // Simple string replacement content = content.replace(editOp.search, editOp.replace); } else if (editOp.mode === 'regex') { // Regex replacement const regex = new RegExp(editOp.search, 'g'); content = content.replace(regex, editOp.replace); } fs.writeFileSync(fullPath, content, 'utf-8'); this.executedOperations.push({ type: 'edit', path: fullPath, mode: editOp.mode, timestamp: new Date().toISOString() }); } /** * Execute command (returns command object for session to execute) */ async executeCommand(cmdOp) { // Commands are executed by the Claude Code session // This just returns the command for the session to handle return { command: cmdOp.command, workingDir: cmdOp.workingDir }; } /** * Get executed operations history */ getExecutedOperations() { return this.executedOperations; } /** * Clear operations history */ clearHistory() { this.executedOperations = []; } } module.exports = { ClaudeXMLParser, ClaudeResponseProcessor };