@wabot-dev/framework
v1.0.10
Published
Framework for IA Chat Bots
Readme
🤖 Wabot
El framework TypeScript para pasar de un bot simple a una aplicación empresarial completa — sin cambiar de herramienta.
Chat multicanal con IA · REST · Socket.IO · UI web · voz por teléfono · jobs & crons · datos con migraciones · auth · observabilidad — todo en un solo proyecto, cableado por decoradores.
⚡ AI-native: tu agente ya sabe construir con Wabot
Wabot trae sus propios skills para agentes de código. Con un comando, tu asistente (Claude Code, Codex, Cursor…) aprende los decoradores, patrones y convenciones del framework — y deja de alucinar APIs.
npx wabot-skills sync # instala/actualiza los skills en tu proyecto
npx wabot-skills list # ver los skills disponiblesSe instalan 15 skills —persistencia, mindsets, agentes, chat, REST/Socket, UI, voz, async, auth, ops, testing, validación, DI/config, design system y una skill paraguas del framework— versionados junto al framework. Actualizas el framework, corres sync, y tu agente sigue al día. Esto acelera tanto a las personas como a los agentes que construyen contigo.
🗺️ Un framework, todas las capas
flowchart TB
A["🤖 Tu app Wabot · un solo proyecto TypeScript"]
A --> C["💬 Chat multicanal + IA<br/>mindsets · agentes · tools"]
A --> W["🌐 Web<br/>REST · Socket.IO · UI (SSR + islands)"]
A --> V["📞 Voz<br/>llamadas Twilio + Realtime"]
A --> J["⏱️ Async<br/>commands · crons · transacciones"]
A --> D["🗄️ Datos<br/>ORM · migraciones · paginación"]
A --> O["🛡️ Producción<br/>auth · logging · OTel · rate limit · shutdown"]Empiezas con un bot de 15 líneas y creces —front web, IA, cron jobs, base de datos, telefonía— reutilizando los mismos servicios, mindset y tests. El runner descubre tu código escaneando src/; cada subsistema es opt-in por decorador. Sin BD, todo corre en memoria; defines DATABASE_URL y el mismo código pasa a Postgres.
🚀 Quickstart
Un proyecto Wabot arranca desde src/_run_.ts:
import { run, IProjectRunnerConfig } from '@wabot-dev/framework'
export const config: IProjectRunnerConfig = {}
export default config
if (process.env.WABOT_BUNDLED !== '1') run(config)run(config) importa todo lo que encuentra en src/ (los decoradores se registran como efecto de import) y levanta cada subsistema. No hay listas de registro manuales.
📘 Para crear un proyecto nuevo con plantilla, sigue la guía de inicio.
💬 Chat multicanal con IA
Define la personalidad y las herramientas del bot en un mindset, y conéctalo a uno o varios canales. El mismo mindset sirve para chat, voz y agentes.
@mindset({ tools: [BacklogTools] })
export class PixelMindset implements IMindset {
async describe(): Promise<IMindsetDescription> {
return {
identity: { name: 'Pixel', language: 'español', personality: 'Tendero 8-bit y sarcástico' },
context: 'El jugador gestiona su backlog de videojuegos contigo.',
skills: 'Listar, agregar, recomendar juegos.',
limits: 'Nunca inventes juegos que el jugador no mencionó.',
workflow: 'Saluda → entiende → gestiona el backlog.',
}
}
async models(): Promise<IMindsetModels> {
return { llm: [{ provider: 'openrouter', model: 'google/gemini-3-flash-preview' }] }
}
}
@chatController()
export class PixelChatController {
constructor(@chatBot(PixelMindset) private pixel: ChatBot) {}
@cmd()
@telegram({ botToken: process.env.TELEGRAM_BOT_TOKEN! })
@whatsApp({ number: '+1555…', accessToken: '…', businessNumberId: '…' })
async onMessage(ctx: IReceivedMessage) {
await this.pixel.sendMessage(ctx.message, async (reply) => ctx.reply(reply))
}
}Las tool functions son métodos con @description(...) sobre una clase @tools(); sus argumentos se validan y tipan automáticamente. El LLM las invoca de verdad.
| Canales | Proveedores de IA |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| Terminal (@cmd) · Socket.IO · Telegram · WhatsApp Cloud API · WaSender | OpenAI · Anthropic (Claude) · Google (Gemini) · OpenRouter · DeepSeek |
🧠 Agentes de IA
Más allá del chat, expón agentes para tu propio código: pídele a un LLM una respuesta tipada, un sí/no, o dale una orden — con contexto y herramientas controladas. Un mindset puede delegar en agentes de forma autónoma (@mindset({ agents })) con gating de tools por agente (allow / deny / budget). Los mismos @tools se reutilizan entre mindset y agentes.
🌐 Web: REST, Socket.IO y UI
REST con validación de DTO integrada y middlewares:
@restController('/orders')
export class OrdersController {
constructor(private orders: OrderService) {}
@onGet('/:id')
async getOne(req: { id: string }) {
return this.orders.find(req.id)
}
@onPost()
@middleware(AuditMiddleware)
async create(req: CreateOrderRequest) {
return this.orders.create(req)
}
}Socket.IO (@socketController, @onSocketEvent, guards de handshake) para tiempo real, con el mismo modelo de validación.
UI server-rendered — un framework de front real, no plantillas: renderiza Preact en el servidor y envía JS solo para las partes interactivas (islands). Escribes @uiController con @view (GET → HTML) y @action (POST → JSON); todo funciona sin JS y las islands son mejora progresiva. Incluye navegación boosted (SPA-like con layout + <Outlet/>), generación estática (SSG), CSS modules y un design system con tokens y componentes accesibles listos para verse bien por defecto.
@uiController('/board')
export class BoardController {
@view('/')
home() {
return <HomePage messages={messages.value} />
}
@action('/add')
add(dto: AddMessageDto) {
addMessage(dto.text!)
return redirect('/board')
}
}📞 Voz por teléfono
Bots de llamadas en tiempo real sobre Twilio, puenteados a un modelo Realtime de OpenAI — con el mismo Mindset como cerebro. Entrantes y salientes, múltiples números/cuentas.
@voiceController()
export class VoiceController {
constructor(@voiceBot(PhoneAssistantMindset) private assistant: VoiceBot) {}
@twilioVoice({ publicBaseUrl: str`public.base.url` })
async onCall(call: IVoiceCall) {
await this.assistant.answer(call, {
greeting: call.greeting ?? 'Saluda breve y cálido, y pregunta en qué puedes ayudar.',
})
}
}⏱️ Async: commands, crons y transacciones
Trabajo en segundo plano tipado: commands (inmediatos o programados), cron handlers, y @transaction() para envolver escrituras en una transacción de BD.
@commandHandler(ChargeOrder)
export class ChargeOrderHandler implements ICommandHandler<ChargeOrder> {
constructor(private payments: PaymentService) {}
async handle(cmd: ChargeOrder) {
await this.payments.charge(cmd.orderId, cmd.amountCents)
}
}
@cronHandler({ name: 'daily-cleanup', cron: '0 3 * * *' })
export class DailyCleanup implements ICronHandler {
async handle() {
/* … */
}
}
// Disparar / programar
await async.runCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 })
await async.scheduleCommand(ChargeOrder, { orderId: 'o_1', amountCents: 2500 }, { minutes: 30 })Sin BD usa stores en memoria; con Postgres, jobs y crons persisten automáticamente.
🗄️ Datos: ORM que escala contigo
Un ORM pequeño: defines una Entity, un @repository con queries por nombre de método, y opcionalmente extensiones por adapter. El backend (memoria o Postgres) lo elige el runner según DATABASE_URL.
@repository({ table: 'game', constructor: Game })
export class GameRepository extends CrudRepository<Game> {
@query() declare findByUserIdAndStatus: (userId: string, status: IGameStatus) => Promise<Game[]>
@query() declare countByStatus: (status: IGameStatus) => Promise<number>
}
// Paginación por cursor (keyset), estable y O(1) sin importar la profundidad
const page = await games.findPage({ limit: 20, cursor })JSONB por defecto, con índices automáticos derivados de tus queries.
Migración a columnas sin tocar la lógica cuando necesitas escalar — mismos
@query, mismos tests. La estrategia se declara en la clase base del repositorio:// Cada campo en una columna real; la tabla es de tus migraciones @repository({ table: 'user', constructor: User }) export class UserRepository extends PgColumnsRepository<User> { @query() declare findByEmail: (email: string) => Promise<User[]> }PgJsonbRepositorydeclara la otra estrategia, yCrudRepositorydeja que el backend elija (JSONB en Postgres). SinDATABASE_URLel fallback a memoria sirve las tres igual, así que la clase base declara intención, no ata el runtime.Proyección opcional con
@repository({ fields }): el repositorio lee y escribe sólo esos campos y el resto de la fila le es invisible — útil sobre tablas anchas o heredadas, y sobre read models. La respeta también el backend en memoria.Migraciones en SQL plano con el CLI
wabot-migrate(forward-only, checksum, advisory locks).Paginación por cursor integrada (
findPage/IPageOptions).
🔐 Auth
Autenticación por JWT (access/refresh) y API keys, en endpoints REST y handshakes de Socket. Un servicio Auth<D> scoping por request:
@injectable()
export class OrdersService {
constructor(private auth: Auth<SessionInfo>) {}
list() {
const session = this.auth.require() // 401 si no hay sesión
return this.repo.findByUserId(session.userId)
}
}Protege rutas con @jwtGuard / @apiKeyGuard (y sus variantes de handshake para Socket).
El token viaja en el header Authorization: Bearer o en una cookie. Cuando conviven varios tipos de usuario en el mismo navegador (por ejemplo un panel admin y el portal de clientes), dale a cada sesión su propia cookie y el guard sólo leerá la suya:
@onPost('/admin/login')
async adminLogin(req: LoginRequest) {
const { access } = await this.jwt.createToken(undefined, { audience: 'admin' })
this.cookies.set('wabot_admin', access.token, { httpOnly: true, expires: access.expiration })
}
@onGet('/admin/orders')
@jwtGuard({ cookie: 'wabot_admin', audience: 'admin' })
list() { ... }
@onPost('/logout')
@jwtGuard({ cookie: ['wabot_admin', 'wabot_client'] }) // cualquiera de las dos
logout() { ... }La cookie evita que las sesiones se pisen; el audience es lo que las aísla de verdad: como todas se firman con el mismo JWT_SECRET, sin aud un token de cliente movido a la cookie de admin pasaría el guard. Con audience:
- el access token lleva el claim
audy sólo lo aceptan los guards que declaran ese mismo valor (un guard sinaudienceacepta cualquier token válido); - el refresh token recuerda su audiencia, así que
findRefreshTokenAuthInfo(secret, { audience: 'admin' })rechaza renovar una sesión de cliente desde el endpoint de admin; - también funciona en sockets:
@jwtHandshakeGuard({ audience: 'admin' }).
Sin cookie, el guard usa JWT_COOKIE_NAME (wabot_jwt por defecto).
Sockets con cookie httpOnly
El handshake acepta el token en handshake.auth, en Authorization o —única forma de usar una cookie que el JS del navegador no puede leer— en la cookie de sesión:
@socketController({ namespace: 'admin' })
@jwtHandshakeGuard({ cookie: 'wabot_admin', audience: 'admin' })
export class AdminSocketController { ... }Leer la cookie exige una allowlist de orígenes (JWT_COOKIE_ALLOWED_ORIGINS=https://app.tudominio.com, o allowedOrigins en el guard). El navegador adjunta la cookie a un WebSocket abierto por cualquier página y no aplica CORS sobre él, así que sin verificar el Origin cualquier sitio podría montarse sobre la sesión (cross-site WebSocket hijacking). El guard falla cerrado: sin allowlist, sin header Origin, con un origen no listado o con *, rechaza el handshake. Los tokens que llegan por handshake.auth no corren ese riesgo y no se verifican por origen.
Del lado del cliente, si el front está en otro dominio: io(url, { withCredentials: true }), cors: { origin: '<origen exacto>', credentials: true } en el server y la cookie con SameSite=None; Secure.
🛡️ Listo para producción
Utilidades transversales pensadas para operar de verdad — todas con implementación en memoria + Postgres, elegida por DATABASE_URL:
- Logging estructurado — legible en dev (
debug), JSON en prod, con correlation ids y niveles configurables (WABOT_LOG_LEVEL). - OpenTelemetry — traces y métricas como peer dependency opcional.
- Ciclo de vida — graceful shutdown central, manejo de crashes,
ShutdownManager.isShuttingDownpara readiness. El drenado está acotado porWABOT_SHUTDOWN_TIMEOUT_SECONDS(30s en producción, 3s fuera) y las conexiones abiertas se cortan pasadoWABOT_HTTP_DRAIN_TIMEOUT_SECONDS(10s / 0.5s), así un stream SSE no deja el Ctrl+C colgado. - Locking distribuido, idempotencia / deduplicación de webhooks, rate limiting (
@rateLimiten REST, 429 + headers). - Config fail-fast — valida referencias tipadas al arrancar (
ConfigError), no en producción a medianoche. CustomErrorcon códigos HTTP,Password(scrypt) yRandomseguro.
🧪 Testing con evals de IA
@wabot-dev/framework/testing prueba chatbots de forma determinista —sin API keys ni BD— y evalúa comportamiento real con un juez LLM.
import { createChatBotHarness, LlmJudge } from '@wabot-dev/framework/testing'
// Determinista: el LLM se simula, tus tools se ejecutan de verdad
const harness = createChatBotHarness({ mindset: PixelMindset })
harness.adapter.callTool('addToBacklog', { title: 'Celeste' }).reply('¡Agregado!')
const turn = await harness.send('agrega Celeste a mi backlog')
// Eval: juzga una conversación real con un LLM
const judge = new LlmJudge({ adapter, models: [{ model: 'claude-haiku-4-5' }] })
await judge.assert({ transcript: harness.history(), criteria: 'Responde en español y confirma' })Hay harnesses para controllers de chat, REST (con guards JWT/API-Key reales), UI, sockets, commands/crons, repositorios en memoria (useMemoryRepositories) y una suite de conformance para adapters LLM propios.
📦 Build & deploy
En dev, el framework descubre módulos escaneando disco. Para producción, empaqueta tu proyecto en un único dist/entry.js autocontenido:
"scripts": {
"dev": "node --import @yucacodes/ts ./src/_run_.ts",
"build": "node ./node_modules/@wabot-dev/framework/dist/build/build.js",
"start": "WABOT_BUNDLED=1 node ./dist/entry.js"
}El build genera imports estáticos (sin readdir ni imports dinámicos en el bundle) y arranca en modo preloaded. Los addons opcionales (pg, SDKs de IA) quedan como peerDependencies y solo entran al bundle si los usas. Ver la sección Building for production en la documentación.
📚 Documentación y skills
- 📖 Documentación oficial — guías completas en español.
- 🧠 Mentalidad del bot · ⚙️ Agregar funcionalidades
- 🧩 Skills del framework (
skills/) — la referencia que también consume tu agente de IA: framework · persistencia · mindset · agentes · chat · rest & socket · ui · voz · async · auth · ops · testing · validación · di & config · design
🛠️ Contribuir & soporte
- 🐛 Bugs: Issues · 💬 Ideas: Discussions
- 📧 Email: [email protected]
📄 Licencia
Hecho con ❤️ por el equipo de Wabot
