import { Database } from 'bun:sqlite' const PORT_HTTP = 4101 // Inicializar base de datos const db = new Database('agent-ui.db') db.run(` CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, tool_name TEXT NOT NULL, args TEXT, result TEXT ) `) db.run(` CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT ) `) // Tabla para componentes Vue dinámicos db.run(` CREATE TABLE IF NOT EXISTS vue_components ( id TEXT PRIMARY KEY, name TEXT NOT NULL, template TEXT NOT NULL, setup TEXT, style TEXT, props TEXT, imports TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) `) // Tabla para temas/estilos db.run(` CREATE TABLE IF NOT EXISTS themes ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, is_default INTEGER DEFAULT 0, is_system INTEGER DEFAULT 0, variables TEXT NOT NULL, metadata TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) `) // Insertar temas del sistema si no existen const existingThemes = db.query('SELECT COUNT(*) as count FROM themes WHERE is_system = 1').get() as { count: number } if (existingThemes.count === 0) { const darkTheme = { id: 'theme-dark', name: 'Dark', description: 'Default dark theme', is_default: 1, is_system: 1, variables: JSON.stringify({ colors: { 'bg-primary': '#0f0f14', 'bg-secondary': '#16161d', 'bg-hover': '#1e1e28', 'bg-tertiary': '#252530', 'border-color': '#2a2a3a', 'border-hover': '#3a3a4a' }, text: { 'text-primary': '#e4e4e7', 'text-secondary': '#a1a1aa', 'text-muted': '#52525b', 'text-inverse': '#0f0f14' }, accent: { 'accent': '#6366f1', 'accent-hover': '#818cf8', 'accent-muted': 'rgba(99, 102, 241, 0.2)', 'accent-text': '#ffffff' }, semantic: { 'success': '#22c55e', 'success-bg': 'rgba(34, 197, 94, 0.1)', 'warning': '#eab308', 'warning-bg': 'rgba(234, 179, 8, 0.1)', 'error': '#ef4444', 'error-bg': 'rgba(239, 68, 68, 0.1)', 'info': '#3b82f6', 'info-bg': 'rgba(59, 130, 246, 0.1)' }, spacing: { 'radius-sm': '4px', 'radius-md': '8px', 'radius-lg': '12px', 'radius-full': '9999px' }, typography: { 'font-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", 'font-mono': "'JetBrains Mono', 'Fira Code', Consolas, monospace" }, effects: { 'shadow-sm': '0 1px 2px rgba(0,0,0,0.2)', 'shadow-md': '0 4px 6px rgba(0,0,0,0.3)', 'shadow-lg': '0 10px 15px rgba(0,0,0,0.4)', 'transition-fast': '0.15s ease', 'transition-normal': '0.2s ease' } }), metadata: JSON.stringify({ author: 'system', version: '1.0.0', tags: ['dark', 'default'] }) } const lightTheme = { id: 'theme-light', name: 'Light', description: 'Clean light theme', is_default: 0, is_system: 1, variables: JSON.stringify({ colors: { 'bg-primary': '#ffffff', 'bg-secondary': '#f4f4f5', 'bg-hover': '#e4e4e7', 'bg-tertiary': '#d4d4d8', 'border-color': '#d4d4d8', 'border-hover': '#a1a1aa' }, text: { 'text-primary': '#18181b', 'text-secondary': '#52525b', 'text-muted': '#a1a1aa', 'text-inverse': '#ffffff' }, accent: { 'accent': '#4f46e5', 'accent-hover': '#4338ca', 'accent-muted': 'rgba(79, 70, 229, 0.1)', 'accent-text': '#ffffff' }, semantic: { 'success': '#16a34a', 'success-bg': 'rgba(22, 163, 74, 0.1)', 'warning': '#ca8a04', 'warning-bg': 'rgba(202, 138, 4, 0.1)', 'error': '#dc2626', 'error-bg': 'rgba(220, 38, 38, 0.1)', 'info': '#2563eb', 'info-bg': 'rgba(37, 99, 235, 0.1)' }, spacing: { 'radius-sm': '4px', 'radius-md': '8px', 'radius-lg': '12px', 'radius-full': '9999px' }, typography: { 'font-sans': "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", 'font-mono': "'JetBrains Mono', 'Fira Code', Consolas, monospace" }, effects: { 'shadow-sm': '0 1px 2px rgba(0,0,0,0.05)', 'shadow-md': '0 4px 6px rgba(0,0,0,0.07)', 'shadow-lg': '0 10px 15px rgba(0,0,0,0.1)', 'transition-fast': '0.15s ease', 'transition-normal': '0.2s ease' } }), metadata: JSON.stringify({ author: 'system', version: '1.0.0', tags: ['light'] }) } const stmt = db.prepare(` INSERT INTO themes (id, name, description, is_default, is_system, variables, metadata) VALUES (?, ?, ?, ?, ?, ?, ?) `) stmt.run(darkTheme.id, darkTheme.name, darkTheme.description, darkTheme.is_default, darkTheme.is_system, darkTheme.variables, darkTheme.metadata) stmt.run(lightTheme.id, lightTheme.name, lightTheme.description, lightTheme.is_default, lightTheme.is_system, lightTheme.variables, lightTheme.metadata) console.log('[DB] Temas del sistema creados') } console.log('[DB] SQLite inicializado: agent-ui.db') // API HTTP solamente - WebSocket lo maneja webmcp Bun.serve({ port: PORT_HTTP, async fetch(req) { const url = new URL(req.url) // CORS headers const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }) } // API Routes if (url.pathname === '/api/history') { if (req.method === 'GET') { const limit = parseInt(url.searchParams.get('limit') || '50') const rows = db.query('SELECT * FROM history ORDER BY id DESC LIMIT ?').all(limit) return Response.json(rows, { headers: corsHeaders }) } if (req.method === 'POST') { const body = await req.json() const stmt = db.prepare('INSERT INTO history (tool_name, args, result) VALUES (?, ?, ?)') stmt.run(body.tool_name, JSON.stringify(body.args), body.result) return Response.json({ success: true }, { headers: corsHeaders }) } if (req.method === 'DELETE') { db.run('DELETE FROM history') return Response.json({ success: true }, { headers: corsHeaders }) } } if (url.pathname === '/api/config') { if (req.method === 'GET') { const key = url.searchParams.get('key') if (key) { const row = db.query('SELECT value FROM config WHERE key = ?').get(key) as { value: string } | null return Response.json({ value: row?.value || null }, { headers: corsHeaders }) } const rows = db.query('SELECT * FROM config').all() return Response.json(rows, { headers: corsHeaders }) } if (req.method === 'POST') { const body = await req.json() const stmt = db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)') stmt.run(body.key, body.value) return Response.json({ success: true }, { headers: corsHeaders }) } } if (url.pathname === '/api/health') { return Response.json({ status: 'ok', timestamp: new Date().toISOString() }, { headers: corsHeaders }) } // API de Componentes Vue if (url.pathname === '/api/components') { if (req.method === 'GET') { const rows = db.query('SELECT * FROM vue_components ORDER BY updated_at DESC').all() return Response.json(rows, { headers: corsHeaders }) } if (req.method === 'POST') { const body = await req.json() const id = body.id || `comp-${Date.now()}` const stmt = db.prepare(` INSERT OR REPLACE INTO vue_components (id, name, template, setup, style, props, imports, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `) stmt.run( id, body.name, body.template, body.setup || '', body.style || '', JSON.stringify(body.props || []), JSON.stringify(body.imports || []) ) return Response.json({ success: true, id }, { headers: corsHeaders }) } if (req.method === 'DELETE') { db.run('DELETE FROM vue_components') return Response.json({ success: true }, { headers: corsHeaders }) } } // Obtener componente por ID if (url.pathname.startsWith('/api/components/')) { const id = url.pathname.split('/').pop() if (req.method === 'GET') { const row = db.query('SELECT * FROM vue_components WHERE id = ?').get(id) if (!row) { return Response.json({ error: 'Component not found' }, { status: 404, headers: corsHeaders }) } return Response.json(row, { headers: corsHeaders }) } if (req.method === 'DELETE') { db.run('DELETE FROM vue_components WHERE id = ?', [id]) return Response.json({ success: true }, { headers: corsHeaders }) } } // ===================== // API de Temas // ===================== // GET /api/themes - Lista todos los temas // POST /api/themes - Crea o actualiza un tema if (url.pathname === '/api/themes') { if (req.method === 'GET') { const rows = db.query('SELECT * FROM themes ORDER BY is_system DESC, is_default DESC, name ASC').all() const themes = (rows as any[]).map(row => ({ ...row, is_default: !!row.is_default, is_system: !!row.is_system, variables: JSON.parse(row.variables), metadata: row.metadata ? JSON.parse(row.metadata) : null })) return Response.json(themes, { headers: corsHeaders }) } if (req.method === 'POST') { const body = await req.json() const id = body.id || `theme-${Date.now()}` const stmt = db.prepare(` INSERT OR REPLACE INTO themes (id, name, description, is_default, is_system, variables, metadata, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `) stmt.run( id, body.name, body.description || '', body.is_default ? 1 : 0, body.is_system ? 1 : 0, JSON.stringify(body.variables), JSON.stringify(body.metadata || {}) ) return Response.json({ success: true, id }, { headers: corsHeaders }) } } // GET /api/themes/active - Obtiene el tema activo (default) if (url.pathname === '/api/themes/active') { if (req.method === 'GET') { const row = db.query('SELECT * FROM themes WHERE is_default = 1 LIMIT 1').get() as any if (!row) { return Response.json({ error: 'No active theme' }, { status: 404, headers: corsHeaders }) } return Response.json({ ...row, is_default: !!row.is_default, is_system: !!row.is_system, variables: JSON.parse(row.variables), metadata: row.metadata ? JSON.parse(row.metadata) : null }, { headers: corsHeaders }) } } // GET /api/design-tokens - Guía de design tokens para LLMs if (url.pathname === '/api/design-tokens') { if (req.method === 'GET') { const row = db.query('SELECT variables FROM themes WHERE is_default = 1 LIMIT 1').get() as { variables: string } | null const tokens = row ? JSON.parse(row.variables) : {} return Response.json({ version: '1.0.0', description: 'Design tokens for Agent UI components. Use these CSS variables for consistent styling.', usage: 'Use var(--token-name) in CSS, e.g., var(--bg-primary)', tokens, guidelines: { backgrounds: 'Use bg-primary for main areas, bg-secondary for cards/panels, bg-tertiary for nested elements', text: 'Use text-primary for headings, text-secondary for body, text-muted for hints', accent: 'Use accent for interactive elements, accent-hover for hover states, accent-muted for backgrounds', semantic: 'Use success/warning/error/info for status indicators with their -bg variants for backgrounds', spacing: 'Use radius-sm (4px) for small elements, radius-md (8px) for cards, radius-lg (12px) for modals', effects: 'Use transition-fast for micro-interactions, shadow-md for elevated elements' }, examples: { button: 'background: var(--accent); color: var(--accent-text); border-radius: var(--radius-md); transition: var(--transition-fast);', card: 'background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);', input: 'background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: var(--radius-md);', badge: 'background: var(--accent-muted); color: var(--accent); padding: 0.25rem 0.5rem; border-radius: var(--radius-full);' } }, { headers: corsHeaders }) } } // Operaciones sobre un tema específico: /api/themes/:id if (url.pathname.startsWith('/api/themes/') && !url.pathname.includes('/active') && !url.pathname.includes('/export')) { const pathParts = url.pathname.split('/') const id = pathParts[3] const action = pathParts[4] // 'default' si existe // POST /api/themes/:id/default - Establecer como default if (action === 'default' && req.method === 'POST') { db.run('UPDATE themes SET is_default = 0') db.run('UPDATE themes SET is_default = 1 WHERE id = ?', [id]) return Response.json({ success: true }, { headers: corsHeaders }) } // GET /api/themes/:id - Obtener un tema if (req.method === 'GET' && !action) { const row = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any if (!row) { return Response.json({ error: 'Theme not found' }, { status: 404, headers: corsHeaders }) } return Response.json({ ...row, is_default: !!row.is_default, is_system: !!row.is_system, variables: JSON.parse(row.variables), metadata: row.metadata ? JSON.parse(row.metadata) : null }, { headers: corsHeaders }) } // DELETE /api/themes/:id - Eliminar un tema if (req.method === 'DELETE' && !action) { // No permitir eliminar temas del sistema const theme = db.query('SELECT is_system FROM themes WHERE id = ?').get(id) as { is_system: number } | null if (theme?.is_system) { return Response.json({ error: 'Cannot delete system theme' }, { status: 403, headers: corsHeaders }) } db.run('DELETE FROM themes WHERE id = ?', [id]) return Response.json({ success: true }, { headers: corsHeaders }) } } // GET /api/themes/export/:id - Exportar tema como JSON if (url.pathname.startsWith('/api/themes/export/')) { const id = url.pathname.split('/').pop() if (req.method === 'GET') { const row = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any if (!row) { return Response.json({ error: 'Theme not found' }, { status: 404, headers: corsHeaders }) } const exportData = { name: row.name, description: row.description, variables: JSON.parse(row.variables), metadata: { ...(row.metadata ? JSON.parse(row.metadata) : {}), exported_at: new Date().toISOString() } } return new Response(JSON.stringify(exportData, null, 2), { headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="${row.name.toLowerCase().replace(/\s+/g, '-')}-theme.json"` } }) } } return new Response('Not Found', { status: 404, headers: corsHeaders }) } }) console.log(`[HTTP] API corriendo en http://localhost:${PORT_HTTP}`) console.log('') console.log('='.repeat(50)) console.log('Agent UI API Server iniciado') console.log(` API: http://localhost:${PORT_HTTP}`) console.log('') console.log('WebMCP se inicia por separado con Claude Code MCP') console.log('='.repeat(50))