- Add dynamicComponents.ts service (~300 lines) - CSS scoping with high specificity - Async setup support with Suspense - Event bus for inter-component communication - Shared Pinia store with main app - No app overhead (uses render + createVNode) - Add MCP tools for Vue components - render_vue_component - save_vue_component - load_vue_component - list_vue_components - delete_vue_component - Add SQLite table for component persistence - Add TypeScript declarations for webmcp - Configure Vite for runtime template compilation - Add comprehensive README with documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166 lines
5.1 KiB
TypeScript
166 lines
5.1 KiB
TypeScript
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
|
|
)
|
|
`)
|
|
|
|
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 })
|
|
}
|
|
}
|
|
|
|
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))
|