feat: AI auto-fix bug tracker with real-time error monitoring

- Real-time error monitoring system with WebSocket
- Auto-fix agent that triggers on browser errors
- Bug tracker dashboard with floating button (🐛)
- Live activity stream showing AI thought process
- Fixed 4 JavaScript errors (SyntaxError, TypeError)
- Fixed SessionPicker API endpoint error
- Enhanced chat input with Monaco editor
- Session picker component for project management

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-21 10:53:11 +00:00
Unverified
parent b765c537fc
commit efb3ecfb19
23 changed files with 7254 additions and 119 deletions

211
server.js
View File

@@ -562,12 +562,63 @@ app.post('/claude/api/claude/sessions', requireAuth, (req, res) => {
}
}
// Create session with projectId in metadata
// ===== Validate and create working directory =====
let validatedWorkingDir = workingDir || VAULT_PATH;
// Resolve to absolute path
const path = require('path');
const fs = require('fs');
const resolvedPath = path.resolve(validatedWorkingDir);
// Security check: ensure path is within allowed boundaries
const allowedPaths = [
VAULT_PATH,
process.env.HOME || '/home/uroma',
'/home/uroma'
];
const isAllowed = allowedPaths.some(allowedPath => {
return resolvedPath.startsWith(allowedPath);
});
if (!isAllowed) {
console.error('[SESSIONS] Working directory outside allowed paths:', resolvedPath);
return res.status(403).json({ error: 'Working directory outside allowed paths' });
}
// Create directory if it doesn't exist
if (!fs.existsSync(resolvedPath)) {
console.log('[SESSIONS] Creating working directory:', resolvedPath);
try {
fs.mkdirSync(resolvedPath, { recursive: true });
} catch (mkdirError) {
console.error('[SESSIONS] Failed to create directory:', mkdirError);
return res.status(400).json({
error: `Failed to create working directory: ${mkdirError.message}`
});
}
}
// Verify it's actually a directory
try {
const stats = fs.statSync(resolvedPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: 'Working directory is not a directory' });
}
} catch (statError) {
console.error('[SESSIONS] Failed to stat directory:', statError);
return res.status(500).json({ error: 'Failed to validate working directory' });
}
console.log('[SESSIONS] Using working directory:', resolvedPath);
// ===== END directory validation =====
// Create session with validated path
const sessionMetadata = {
...metadata,
...(validatedProjectId ? { projectId: validatedProjectId } : {})
};
const session = claudeService.createSession({ workingDir, metadata: sessionMetadata });
const session = claudeService.createSession({ workingDir: resolvedPath, metadata: sessionMetadata });
// Store session in database with projectId
db.prepare(`
@@ -777,6 +828,95 @@ app.post('/claude/api/claude/sessions/:id/duplicate', requireAuth, (req, res) =>
}
});
// Fork session from a specific message index
app.post('/claude/api/claude/sessions/:id/fork', requireAuth, (req, res) => {
try {
const sessionId = req.params.id;
const messageIndex = parseInt(req.query.messageIndex) || -1; // -1 means all messages
// Get source session
let sourceSession = claudeService.sessions.get(sessionId);
if (!sourceSession) {
return res.status(404).json({ error: 'Source session not found' });
}
// Security check: validate workingDir is within VAULT_PATH
const fullPath = path.resolve(sourceSession.workingDir);
if (!fullPath.startsWith(VAULT_PATH)) {
return res.status(400).json({ error: 'Invalid working directory' });
}
// Get messages to fork (1..messageIndex)
let messagesToFork = [];
if (sourceSession.outputBuffer && sourceSession.outputBuffer.length > 0) {
if (messageIndex === -1) {
// Fork all messages
messagesToFork = [...sourceSession.outputBuffer];
} else {
// Fork messages up to messageIndex
messagesToFork = sourceSession.outputBuffer.slice(0, messageIndex + 1);
}
}
// Get projectId from source session (if exists)
const sourceProjectId = sourceSession.metadata.projectId || null;
// Create new session with forked context
const newSession = claudeService.createSession({
workingDir: sourceSession.workingDir,
metadata: {
...sourceSession.metadata,
forkedFrom: sessionId,
forkedAtMessageIndex: messageIndex,
forkedAt: new Date().toISOString(),
source: 'web-ide'
}
});
// Initialize output buffer with forked messages
if (messagesToFork.length > 0) {
newSession.outputBuffer = messagesToFork;
}
// Store forked session in database with same projectId as source
db.prepare(`
INSERT INTO sessions (id, projectId, deletedAt)
VALUES (?, ?, NULL)
`).run(newSession.id, sourceProjectId);
// Update project's lastActivity if session was assigned to a project
if (sourceProjectId) {
const now = new Date().toISOString();
db.prepare(`
UPDATE projects
SET lastActivity = ?
WHERE id = ?
`).run(now, sourceProjectId);
}
console.log(`[FORK] Forked session ${sessionId} at message ${messageIndex} -> ${newSession.id}`);
console.log(`[FORK] Copied ${messagesToFork.length} messages`);
res.json({
success: true,
session: {
id: newSession.id,
pid: newSession.pid,
workingDir: newSession.workingDir,
status: newSession.status,
createdAt: newSession.createdAt,
metadata: newSession.metadata,
projectId: sourceProjectId,
messageCount: messagesToFork.length
}
});
} catch (error) {
console.error('Error forking session:', error);
res.status(500).json({ error: 'Failed to fork session' });
}
});
// Delete session
app.delete('/claude/api/claude/sessions/:id', requireAuth, (req, res) => {
try {
@@ -1824,19 +1964,24 @@ wss.on('connection', (ws, req) => {
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
console.log('WebSocket message received:', data.type);
console.log('[WebSocket] Message received:', {
type: data.type,
sessionId: data.sessionId?.substring(0, 20) || 'none',
hasCommand: !!data.command,
commandLength: data.command?.length || 0
});
if (data.type === 'command') {
const { sessionId, command } = data;
console.log(`Sending command to session ${sessionId}: ${command.substring(0, 50)}...`);
console.log(`[WebSocket] Sending command to session ${sessionId}: ${command.substring(0, 50)}...`);
// Send command to Claude Code
try {
claudeService.sendCommand(sessionId, command);
console.log(`Command sent successfully to session ${sessionId}`);
console.log(`[WebSocket] ✓ Command sent successfully to session ${sessionId}`);
} catch (error) {
console.error('Error sending command:', error);
console.error(`[WebSocket] ✗ Error sending command:`, error.message);
ws.send(JSON.stringify({
type: 'error',
error: error.message
@@ -1848,11 +1993,11 @@ wss.on('connection', (ws, req) => {
const client = clients.get(clientId);
if (client) {
client.sessionId = sessionId;
console.log(`Client ${clientId} subscribed to session ${sessionId}`);
console.log(`[WebSocket] Client ${clientId} subscribed to session ${sessionId}`);
}
}
} catch (error) {
console.error('WebSocket error:', error);
console.error('[WebSocket] Error:', error);
ws.send(JSON.stringify({
type: 'error',
error: error.message
@@ -1962,6 +2107,56 @@ claudeService.on('operations-error', (data) => {
console.log(`WebSocket server running on ws://localhost:${PORT}/claude/api/claude/chat`);
// Real-time error monitoring endpoint
app.post('/claude/api/log-error', express.json(), (req, res) => {
const error = req.body;
const timestamp = new Date().toISOString();
// Log with visual marker for Claude to notice
console.error('\n🚨 [BROWSER_ERROR] ' + '='.repeat(60));
console.error('🚨 [BROWSER_ERROR] Time:', timestamp);
console.error('🚨 [BROWSER_ERROR] Type:', error.type);
console.error('🚨 [BROWSER_ERROR] Message:', error.message);
if (error.url) console.error('🚨 [BROWSER_ERROR] URL:', error.url);
if (error.line) console.error('🚨 [BROWSER_ERROR] Line:', error.line);
if (error.stack) console.error('🚨 [BROWSER_ERROR] Stack:', error.stack);
console.error('🚨 [BROWSER_ERROR] ' + '='.repeat(60) + '\n');
// Also write to error log file
const fs = require('fs');
const errorLogPath = '/tmp/browser-errors.log';
const errorEntry = JSON.stringify({ ...error, loggedAt: timestamp }) + '\n';
fs.appendFileSync(errorLogPath, errorEntry);
// 🤖 AUTO-TRIGGER: Spawn auto-fix agent in background
const { spawn } = require('child_process');
const agentPath = '/home/uroma/obsidian-web-interface/scripts/auto-fix-agent.js';
console.log('🤖 [AUTO_FIX] Triggering agent to fix error...');
const agent = spawn('node', [agentPath, 'process'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, ERROR_DATA: JSON.stringify(error) }
});
agent.stdin.write(JSON.stringify(error));
agent.stdin.end();
agent.stdout.on('data', (data) => {
console.log('🤖 [AUTO_FIX]', data.toString());
});
agent.stderr.on('data', (data) => {
console.error('🤖 [AUTO_FIX] ERROR:', data.toString());
});
agent.on('close', (code) => {
console.log(`🤖 [AUTO_FIX] Agent exited with code ${code}`);
});
res.json({ received: true, autoFixTriggered: true });
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.log('\nShutting down gracefully...');