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>
This commit is contained in:
uroma
2026-01-19 16:29:44 +00:00
Unverified
commit 0dd2083556
44 changed files with 18955 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
const fs = require('fs').promises;
const fsSync = require('fs');
const path = require('path');
const { spawn } = require('child_process');
const { extractAllTags } = require('./tag-parser');
/**
* Response Processor - Executes dyad-style tags
* Handles file operations, package installation, and commands
*/
class ResponseProcessor {
constructor(vaultPath) {
this.vaultPath = vaultPath;
}
/**
* Process all tags from a response
* Executes operations in order: delete → rename → write → dependencies → commands
*/
async processResponse(sessionId, response, options = {}) {
const {
workingDir = this.vaultPath,
autoApprove = false,
onProgress = null
} = options;
const tags = extractAllTags(response);
// Track operations
const results = {
sessionId,
workingDir,
operations: [],
errors: [],
timestamp: new Date().toISOString()
};
// Execute in order: deletes first, then renames, then writes
for (const tag of tags.deletes) {
try {
if (onProgress) onProgress({ type: 'delete', path: tag.path });
const result = await this.executeDelete(workingDir, tag);
results.operations.push(result);
} catch (error) {
results.errors.push({ type: 'delete', tag, error: error.message });
}
}
for (const tag of tags.renames) {
try {
if (onProgress) onProgress({ type: 'rename', from: tag.from, to: tag.to });
const result = await this.executeRename(workingDir, tag);
results.operations.push(result);
} catch (error) {
results.errors.push({ type: 'rename', tag, error: error.message });
}
}
for (const tag of tags.writes) {
try {
if (onProgress) onProgress({ type: 'write', path: tag.path });
const result = await this.executeWrite(workingDir, tag);
results.operations.push(result);
} catch (error) {
results.errors.push({ type: 'write', tag, error: error.message });
}
}
for (const tag of tags.dependencies) {
try {
if (onProgress) onProgress({ type: 'install', packages: tag.packages });
const result = await this.executeAddDependency(workingDir, tag);
results.operations.push(result);
} catch (error) {
results.errors.push({ type: 'install', tag, error: error.message });
}
}
for (const tag of tags.commands) {
try {
if (onProgress) onProgress({ type: 'command', command: tag.command });
const result = await this.executeCommand(workingDir, tag);
results.operations.push(result);
} catch (error) {
results.errors.push({ type: 'command', tag, error: error.message });
}
}
return results;
}
/**
* Execute dyad-write tag
*/
async executeWrite(workingDir, tag) {
const fullPath = path.join(workingDir, tag.path);
// Security check
if (!fullPath.startsWith(workingDir)) {
throw new Error('Access denied: path outside working directory');
}
// Create directory if needed
const dir = path.dirname(fullPath);
if (!fsSync.existsSync(dir)) {
await fs.mkdir(dir, { recursive: true });
}
// Write file
await fs.writeFile(fullPath, tag.content, 'utf-8');
return {
type: 'write',
path: tag.path,
fullPath,
success: true
};
}
/**
* Execute dyad-rename tag
*/
async executeRename(workingDir, tag) {
const fromPath = path.join(workingDir, tag.from);
const toPath = path.join(workingDir, tag.to);
// Security check
if (!fromPath.startsWith(workingDir) || !toPath.startsWith(workingDir)) {
throw new Error('Access denied: path outside working directory');
}
// Check if source exists
if (!fsSync.existsSync(fromPath)) {
throw new Error(`Source file not found: ${tag.from}`);
}
// Create target directory if needed
const toDir = path.dirname(toPath);
if (!fsSync.existsSync(toDir)) {
await fs.mkdir(toDir, { recursive: true });
}
// Rename/move file
await fs.rename(fromPath, toPath);
return {
type: 'rename',
from: tag.from,
to: tag.to,
fromPath,
toPath,
success: true
};
}
/**
* Execute dyad-delete tag
*/
async executeDelete(workingDir, tag) {
const fullPath = path.join(workingDir, tag.path);
// Security check
if (!fullPath.startsWith(workingDir)) {
throw new Error('Access denied: path outside working directory');
}
// Check if exists
if (!fsSync.existsSync(fullPath)) {
throw new Error(`File not found: ${tag.path}`);
}
// Delete file or directory
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
await fs.rm(fullPath, { recursive: true, force: true });
} else {
await fs.unlink(fullPath);
}
return {
type: 'delete',
path: tag.path,
fullPath,
success: true
};
}
/**
* Execute dyad-add-dependency tag
*/
async executeAddDependency(workingDir, tag) {
const packageJsonPath = path.join(workingDir, 'package.json');
// Check if package.json exists
if (!fsSync.existsSync(packageJsonPath)) {
throw new Error('No package.json found in working directory');
}
// Install packages using npm
return new Promise((resolve, reject) => {
const packages = tag.packages.join(' ');
const npm = spawn('npm', ['install', '--save', ...tag.packages], {
cwd: workingDir,
stdio: ['ignore', 'pipe', 'pipe'],
shell: true
});
let output = '';
let errorOutput = '';
npm.stdout.on('data', (data) => {
output += data.toString();
});
npm.stderr.on('data', (data) => {
errorOutput += data.toString();
});
npm.on('close', (code) => {
if (code === 0) {
resolve({
type: 'install',
packages: tag.packages,
output,
success: true
});
} else {
reject(new Error(`npm install failed: ${errorOutput}`));
}
});
npm.on('error', (error) => {
reject(new Error(`Failed to spawn npm: ${error.message}`));
});
});
}
/**
* Execute dyad-command tag
* Currently supports: rebuild, restart, refresh
*/
async executeCommand(workingDir, tag) {
const commands = {
rebuild: async () => {
// Rebuild logic - could be project-specific
return {
type: 'command',
command: tag.command,
result: 'Rebuild triggered',
success: true
};
},
restart: async () => {
// Restart dev server logic
return {
type: 'command',
command: tag.command,
result: 'Restart triggered',
success: true
};
},
refresh: async () => {
// Refresh preview logic
return {
type: 'command',
command: tag.command,
result: 'Refresh triggered',
success: true
};
}
};
const handler = commands[tag.command];
if (!handler) {
throw new Error(`Unknown command: ${tag.command}`);
}
return await handler();
}
/**
* Preview operations without executing
*/
async previewOperations(response, workingDir = this.vaultPath) {
const tags = extractAllTags(response);
const { generateOperationSummary } = require('./tag-parser');
return {
workingDir,
operations: generateOperationSummary(tags),
hasChanges: tags.writes.length > 0 || tags.renames.length > 0 || tags.deletes.length > 0,
timestamp: new Date().toISOString()
};
}
}
module.exports = ResponseProcessor;