Initial commit: SKEEN Derma Experts - Sistema Integral de Gestión Clínica

- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind
- Frontend Homenest (versión alternativa)
- Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp)
- WACRM fork (Next.js 16 + Supabase)
- Hermes + Bridge + Skills (Qwen3.6 via Nan Builders)
- Scripts de migración y operación
- Documentación extensiva en docs/
This commit is contained in:
2026-07-20 07:44:23 +00:00
commit a718592291
699 changed files with 324602 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
version: '3.8'
services:
# PostgreSQL (reemplaza a Supabase)
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
# Redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# WACRM (Next.js)
wacrm:
build:
context: ./skeen-wacrm
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- NEXT_PUBLIC_APP_URL=http://localhost:3000
- NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
- NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
- SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
depends_on:
- postgres
- redis
volumes:
- ./skeen-wacrm:/app
- /app/node_modules
# Hermes (Python)
hermes:
build:
context: ./hermes
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- HERMES_MODEL_PROVIDER=openai
- HERMES_MODEL=gpt-4o
- OPENAI_API_KEY=${OPENAI_API_KEY:-sk-test}
- HERMES_WEBHOOK_PORT=8080
- HERMES_API_SERVER_ENABLED=true
- HERMES_CRON_ENABLED=true
- WACRM_URL=http://wacrm:3000
- WACRM_API_KEY=wacrm_dev_key
volumes:
- ./hermes/.hermes:/root/.hermes
- ./hermes/skills:/skills:ro
depends_on:
- wacrm
volumes:
postgres_data:
redis_data:

View File

@@ -0,0 +1,759 @@
# ROADMAP SKEEN Derma Experts: WACRM + Hermes + Odoo
## Arquitectura Tecnica Actualizada | Reemplazo Twilio → WACRM | FastAPI → Hermes
**Consultoria Alcaraz Salazar, S.A.S.**
**5 de julio de 2026**
---
## Tabla de Contenidos
1. Comparativa: Arquitectura Original vs Nueva
2. Arquitectura Detallada con WACRM + Hermes
3. Stack Tecnologico Actualizado
4. Estructura de Proyecto Actualizada
5. Skills de Hermes para SKEEN
6. Integraciones Detalladas
7. Configuracion de WACRM
8. Configuracion de Hermes
9. Roadmap Actualizado por Fase
10. Ventajas y Desventajas de esta Arquitectura
11. Requisitos del Cliente (Actualizados)
12. Presupuesto de Infraestructura Actualizado
---
## 1. Comparativa: Arquitectura Original vs Nueva
### 1.1 Tabla Comparativa General
| Componente | Arquitectura ORIGINAL | Arquitectura NUEVA (WACRM + Hermes) | Justificacion |
|------------|----------------------|-------------------------------------|---------------|
| WhatsApp Gateway | Twilio API ($0.005/msg) + webhook custom | WACRM + Meta WhatsApp Cloud API directo | Sin intermediario, inbox nativo, costo solo Meta fees (~$0.003-0.008/msg) |
| Inbox / CRM | Inbox custom en React (a construir) | WACRM shared inbox (listo) | Inbox multi-agente con asignacion, notas internas, Kanban pipelines, broadcast — sin desarrollo |
| Backend IA | FastAPI custom + GPT-4o directo | Hermes gateway + skills (SKILL.md) | Agente autonomo con 25+ tools, memoria persistente SQLite, cron nativo, multi-model, subagentes |
| Agente "Sofia" | API endpoint REST stateless | Agente autonomo Hermes con contexto | Memoria persistente, proactive (cron), skills modulares, handoff a humano, context compression |
| Pipelines de ventas | A construir en Odoo/Odoo + custom | WACRM Kanban pipelines nativos | Drag-drop, deals vinculados a conversaciones, win-rate analytics |
| Base de datos CRM | PostgreSQL custom | Supabase (PostgreSQL + RLS + Auth) | Auth built-in, Row-Level Security, Realtime subscriptions, Storage |
| Automatizaciones | Codigo custom en FastAPI | WACRM no-code automations + Hermes skills | Visual builder (Figma-like) + agente IA con razonamiento |
| Broadcasts | Twilio API custom | WACRM broadcasts con templates Meta | Templates aprobados por Meta, delivery tracking, segmentacion |
| Contactos / Leads | Tabla custom en PostgreSQL | WACRM contacts + tags + custom fields | Deduplicacion automatica, CSV import, tags, campos custom |
| AI Assistant | GPT-4o directo via API | WACRM AI Assistant (BYOK) + Hermes skills | Draft replies, auto-reply bot, knowledge base, handoff a humano |
| Costo mensual infra | ~$150-250 USD/mes | ~$80-150 USD/mes | WACRM es MIT (gratis), Hermes es MIT (gratis), solo hosting + API fees |
| Tiempo de desarrollo inbox | ~2 semanas | ~0 dias (listo) | WACRM inbox funcional dia 1 post-configuracion |
| Tiempo de desarrollo agente IA | ~3 semanas | ~1 semana (skills + config) | Hermes ya tiene tools, memoria, gateway, cron — solo configurar skills |
### 1.2 Diferencias Clave por Dimension
| Dimension | Original | Nueva | Impacto |
|-----------|----------|-------|---------|
| Complejidad de codigo | Alto (inbox + agente + CRM desde cero) | Medio (solo skills + integraciones) | ~60% menos codigo custom |
| Mantenimiento | Equipo propio mantiene todo | Comunidad open-source mantiene WACRM/Hermes | Menos deuda tecnica |
| Escalabilidad | Manual | WACRM escala con Supabase; Hermes con gateway | Mejor arquitectura cloud-native |
| Vendor lock-in | Twilio (API propietaria) | Meta Cloud API (estandar) + MIT code | Codigo propio, migrable |
| Onboarding de agentes | Desarrollo custom | WACRM invita por link, roles nativos | Minutos vs dias |
| Analytics | A construir | WACRM dashboard real-time nativo | Disponible dia 1 |
| Subagentes | No disponible | Sí — delegacion aislada con terminal propio | Tareas paralelas sin costo de contexto |
| Fallback providers | No disponible | Sí — cadena de failover automatico | Mayor resiliencia |
| Context compression | No disponible | Sí nativo — hasta 50% de ventana | Manejo de conversaciones largas |
| Credential pools | No disponible | Sí — rotacion de API keys | Mayor tolerancia a rate limits |
### 1.3 Mapa de Reemplazo de Componentes
```
Twilio API ──────────────→ Meta WhatsApp Cloud API (via WACRM)
Twilio webhooks ─────────→ WACRM webhooks (/api/v1/webhooks)
Inbox React custom ──────→ WACRM shared inbox (Next.js 16 App Router)
FastAPI backend ─────────→ Hermes gateway (Python daemon)
GPT-4o directo ──────────→ Hermes agent runtime (multi-model + fallback)
PostgreSQL CRM ──────────→ Supabase PostgreSQL + RLS
Automatizaciones code ───→ WACRM no-code automations + Hermes skills + cron
Dashboard analytics ─────→ WACRM dashboard real-time
Contact management ──────→ WACRM contacts + tags + pipelines
OpenClaw gateway ────────→ Hermes gateway (Python, mas plataformas nativas)
OpenClaw heartbeat ───────→ Hermes cron nativo (mas flexible)
OpenClaw skills ─────────→ Hermes skills (compatible agentskills.io)
OpenClaw memory MD ──────→ Hermes SQLite + MEMORY.md + plugins
```
---
## 2. Arquitectura Detallada con WACRM + Hermes
### 2.1 Diagrama de Componentes Completo
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER: PACIENTE │
│ WhatsApp Business (telefono SKEEN) ─── App movil del paciente │
└─────────────────────────────────────────────────────────────────────────────┘
▼ Meta Cloud API (HTTPS webhook)
┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER: WACRM (CRM) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ WACRM — Next.js 16 + Supabase PostgreSQL │ │
│ │ Puerto: 3000 (interno) / HTTPS via Nginx │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Shared Inbox │ │ Contacts │ │ Sales Pipelines (Kanban)│ │ │
│ │ │ Multi-agente │ │ + Tags + │ │ Deals → Conversaciones │ │ │
│ │ │ Asignacion │ │ Custom Fields│ │ Win-rate analytics │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Broadcasts │ │ Automations │ │ AI Assistant (BYOK) │ │ │
│ │ │ Templates │ │ No-code │ │ OpenAI/Anthropic key │ │ │
│ │ │ Meta │ │ Triggers │ │ Auto-reply + Handoff │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Dashboard │ │ Flows │ │ Templates Manager │ │ │
│ │ │ Real-time │ │ Chatbots │ │ Meta approval sync │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ API REST PUBLICA: /api/v1 │ │
│ │ ├── POST /messages (enviar WhatsApp) │ │
│ │ ├── GET /contacts (listar contactos) │ │
│ │ ├── POST /contacts (crear/find-or-create) │ │
│ │ ├── GET /conversations (listar conversaciones) │ │
│ │ ├── GET /conversations/:id/messages (thread) │ │
│ │ ├── POST /broadcasts (campaña broadcast) │ │
│ │ ├── POST /webhooks (registrar webhook outbound) │ │
│ │ └── GET /me (info cuenta + scopes) │ │
│ │ │ │
│ │ Webhooks outbound (a Hermes): │ │
│ │ ├── message.received (nuevo mensaje) │ │
│ │ ├── message.status_updated (delivery/read) │ │
│ │ └── conversation.created (nueva conversacion) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌─────────────────┐ ┌──────────────────────────┐
│ LAYER: Hermes │ │ LAYER: Odoo 17 │ │ LAYER: Frontend React │
│ (Agente IA "Sofia") │ │ (Backend ERP) │ │ (app.skeen.mx) │
│ │ │ │ │ │
│ Gateway Python │ │ Modulos: │ │ Dashboard clinicos │
│ Puerto: 8080 (HTTP) │ │ ├── Pacientes │ │ Agenda citas │
│ + WS API (cron) │ │ ├── Citas │ │ Monedero digital │
│ Daemon (systemd) │ │ ├── Historial │ │ Pagos (Stripe/MP) │
│ │ │ ├── Inventario │ │ Reportes │
│ ┌──────────────────┐ │ │ ├── POS │ │ │
│ │ Agent Runtime │ │ │ ├── Monedero │ │ Auth: Odoo JWT │
│ │ ├─ SOUL.md │ │ │ └── Website │ │ API: Odoo REST + │
│ │ ├─ AGENTS.md │ │ │ │ │ WACRM REST │
│ │ ├─ SKILL.md (5) │ │ │ API REST XML/ │ │ │
│ │ ├─ MEMORY.md │ │ │ JSON (custom) │ │ Puerto: 5173 (dev) │
│ │ └─ SQLite state │ │ │ │ │ 80/443 (prod) │
│ └──────────────────┘ │ └─────────────────┘ └──────────────────────────┘
│ │
│ ┌──────────────────┐ │
│ │ Cron (nativo) │ │
│ │ ├─ Recordatorios│ │
│ │ ├─ Follow-ups │ │
│ │ └─ Re-engagement│ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Tools Built-in │ │
│ │ ├─ web_search │ │
│ │ ├─ web_fetch │ │
│ │ ├─ exec (python)│ │
│ │ ├─ shell │ │
│ │ ├─ browser │ │
│ │ └─ file R/W │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Custom Plugins │ │
│ │ ├─ odoo_api_call│ │
│ │ ├─ wacrm_send │ │
│ │ └─ calendar_check│ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ API Server │ │
│ │ /v1/chat/completions│ │
│ │ /api/jobs (cron)│ │
│ └──────────────────┘ │
└───────────────────────┘
```
### 2.2 Flujo de Datos Paso a Paso
#### FLUJO 1: Primera vez que un paciente escribe por WhatsApp
```
[1] Paciente envia "Hola, quiero agendar una cita" por WhatsApp
[2] Meta WhatsApp Cloud API recibe el mensaje
[3] Meta envia webhook HTTPS a WACRM: POST /api/whatsapp/webhook
[4] WACRM procesa:
- Crea/actualiza contacto (telefono, nombre si disponible)
- Crea conversacion en inbox
- Almacena mensaje en Supabase
- Dispara trigger de automatizacion ("Nuevo mensaje")
├──► WACRM AI Assistant (opcional, si auto-reply activo)
└──► Webhook outbound a Hermes: POST http://hermes:8080/webhook/wacrm
[5] Hermes Gateway recibe webhook via HTTP endpoint custom
- Parsea payload: { event: "message.received", data: { from, text, conversation_id } }
- Inicia/continua sesion para ese contacto (SQLite session store)
- Inyecta skills relevantes al contexto
[6] Hermes Agent Runtime procesa con LLM (GPT-4o):
- Lee skills: skeen-rag.md (catalogo) + skeen-agendar.md (citas)
- Detecta intencion: "agendar_cita"
- Consulta disponibilidad via plugin odoo_api_call
[7] Hermes → Odoo API (XML-RPC/JSON-RPC):
- Consulta slots disponibles del dermatologo
- Recibe: ["2026-07-05 10:00", "2026-07-05 14:00", "2026-07-06 09:00"]
[8] Hermes formatea respuesta y envia a WACRM:
POST /api/v1/messages
{ "to": "+526641234567", "type": "text",
"text": "Hola! Soy Sofia de SKEEN. Puedo agendarte para:
1. Sab 5 jul 10:00 AM
2. Sab 5 jul 2:00 PM
3. Dom 6 jul 9:00 AM
Cual prefieres?" }
[9] WACRM envia via Meta Cloud API → WhatsApp → Paciente
[10] Respuesta del paciente: "El sabado a las 10"
└──► Repite [2]→[6], Hermes confirma cita
└──► Odoo: crea cita + WACRM: envia confirmacion
```
#### FLUJO 2: Confirmacion de cita + Recordatorio automatico
```
[1] Hermes crea la cita en Odoo via odoo_api_call
- Modelo: salon.booking (o calendar.event)
- Datos: paciente_id, servicio_id, fecha_hora, estado='confirmada'
[2] Hermes programa recordatorio via cron nativo:
- hermes cron schedule --name "recordatorio-cita-[ID]" \
--at "2026-07-04 18:00" \
--command "skill skeen-recordatorios --phone +526641234567 --cita 2026-07-05T10:00"
[3] Cron de Hermes ejecuta a la hora programada:
- Skill skeen-recordatorios se activa
- Consulta Odoo: citas para manana (5 jul)
- Para cada cita sin recordatorio enviado:
└──► WACRM API: POST /api/v1/messages
{ "to": "+526641234567", "type": "template",
"template": { "name": "recordatorio_cita_skeen",
"language": "es_MX",
"params": ["Sofia", "5 de julio", "10:00 AM"] } }
[4] WACRM envia template aprobado por Meta → WhatsApp → Paciente
"Hola Sofia! Te recordamos tu cita manana 5 de julio a las 10:00 AM
en SKEEN Derma Experts. Ubicacion: Blvd. Popotla 123, Rosarito."
[5] Odoo marca recordatorio como enviado
```
#### FLUJO 3: Pago de cita + Monedero digital
```
[1] Paciente despues de cita: "Quiero pagar mi tratamiento"
[2] WACRM → Hermes (webhook message.received)
[3] Hermes skill skeen-pagos.md:
- Consulta Odoo: citas del paciente + servicios pendientes de pago
- Calcula total
- Genera link de pago (Stripe Checkout / MercadoPago preference)
[4] Hermes responde via WACRM:
"Tu tratamiento de Acne Control tiene un saldo de $1,200 MXN.
Puedes pagar aqui: [Link Stripe] o [Link MercadoPago]
Tambien acumulas 120 puntos en tu monedero SKEEN!"
[5] Paciente paga via Stripe/MP
[6] Webhook Stripe/MP → Odoo (confirmacion de pago)
- Odoo actualiza estado de factura
- Odoo acumula puntos en monedero del paciente
[7] Hermes cron detecta pago confirmado:
- WACRM: envia confirmacion + saldo de monedero
"Pago confirmado! Gracias Sofia. Tu monedero SKEEN: 450 puntos
(equivalente a $450 MXN). Los puedes usar en tu proxima visita."
```
### 2.3 Diagrama de Secuencia: Agendar Cita Completa
```
Paciente WhatsApp Meta API WACRM Hermes Odoo
│ │ │ │ │ │
│ "Hola, quiero │ │ │ │ │
│ agendar" │ │ │ │ │
│─────────────────>│ │ │ │ │
│ │ │ │ │ │
│ │ Webhook msg │ │ │ │
│ │<───────────────│ │ │ │
│ │ │ │ │ │
│ │ │ POST /webhook │ │
│ │ │─────────────>│ │ │
│ │ │ │ message.received │
│ │ │ │ webhook outbound │
│ │ │ │──────────────>│ │
│ │ │ │ │ │
│ │ │ │ │ odoo_api_call│
│ │ │ │ │ (slots libres)│
│ │ │ │ │─────────────>│
│ │ │ │ │ │
│ │ │ │ │<─────────────│
│ │ │ │ │ ["10:00", │
│ │ │ │ │ "14:00"] │
│ │ │ │ │ │
│ │ │ POST /api/v1/messages │ │
│ │ │<─────────────│ │ │
│ │ │ (WACRM envia opciones) │ │
│ │ │ │ │ │
│ │ Template msg │ │ │ │
│ │<───────────────│ │ │ │
│ "Hola! Opciones:│ │ │ │ │
│ 1. 10:00..." │ │ │ │ │
│<─────────────────│ │ │ │ │
│ │ │ │ │ │
│ "Opcion 1" │ │ │ │ │
│─────────────────>│ │ │ │ │
│ │ ... (mismo flujo) ... │ │ │
│ │ │ │ │ │
│ │ │ │ │ odoo_api_call│
│ │ │ │ │ (crear cita) │
│ │ │ │ │─────────────>│
│ │ │ │ │ │
│ │ │ POST /api/v1/messages │ │
│ │ │<─────────────│ │ │
│ │ │ │ │ │
│ "Cita confirmada│ │ │ │ │
│ para 5 jul 10AM│ │ │ │ │
│ con Dra. Ana" │ │ │ │ │
│<─────────────────│ │ │ │ │
│ │ │ │ │ │
```
### 2.4 Comunicacion entre Componentes
| De | A | Protocolo | Auth | Payload |
|----|---|-----------|------|---------|
| Meta API | WACRM | HTTPS webhook | Verify Token HMAC | JSON mensaje |
| WACRM | Meta API | HTTPS REST | Bearer Access Token | JSON mensaje/template |
| WACRM | Hermes | HTTPS webhook outbound | HMAC-SHA256 signature | JSON evento |
| Hermes | WACRM | HTTPS REST | Bearer API Key (wacrm_live_*) | JSON mensaje |
| Hermes | Odoo | XML-RPC / JSON-RPC / HTTP | API Key / OAuth | XML/JSON |
| Odoo | Frontend | HTTPS REST (CORS) | JWT Token | JSON |
| Frontend | WACRM | HTTPS REST | Bearer API Key | JSON (dashboard unificado) |
| Hermes | LLM Provider | HTTPS SSE/REST | Bearer API Key | JSON (OpenAI/Anthropic format) |
| Hermes | Hermes API | HTTPS REST | Bearer Token | JSON (OpenAI-compatible) |
---
## 3. Stack Tecnologico Actualizado
### 3.1 Tabla Completa de Tecnologias
#### WACRM (CRM WhatsApp)
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| Framework | Next.js | 16 (App Router) | SSR + API routes + ISR | MIT / Gratis |
| UI | React | 19 | Componentes interactivos | MIT / Gratis |
| Lenguaje | TypeScript | 5.x | Tipado estatico | OSS / Gratis |
| Estilos | Tailwind CSS | v4 | Utility-first CSS | MIT / Gratis |
| UI Kit | shadcn/ui + base-ui | latest | Componentes accesibles | MIT / Gratis |
| Database | Supabase (PostgreSQL) | 15+ | Datos + Auth + RLS + Realtime | OSS Cloud / ~$25/mes |
| Auth | Supabase Auth | built-in | Email/password + JWT | Incluido |
| Storage | Supabase Storage | built-in | Avatares + media | Incluido |
| WhatsApp | Meta Cloud API | v18.0+ | Mensajeria oficial | Meta fees |
| Encriptacion | node:crypto | built-in | AES-256-GCM tokens | built-in |
#### Hermes (Agente IA)
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| Runtime | Python | 3.11+ | Gateway daemon | OSS / Gratis |
| Package | uv / pip | latest | Gestor de paquetes | OSS / Gratis |
| Gateway | hermes-agent | 0.14.0+ | Gateway HTTP + WS + API | MIT / Gratis |
| Comunicacion | HTTP / WebSocket | RFC 6455 | Gateway ↔ clients | built-in |
| Skills | SKILL.md (Markdown) | agentskills.io | Capacidades del agente | MIT / Gratis |
| Memory | SQLite + MEMORY.md | - | Memoria persistente + FTS5 search | File system |
| Cron | hermes cron | built-in | Tareas programadas | built-in |
| LLM Provider | OpenAI GPT-4o | latest | Modelo principal | ~$0.005-0.015/token |
| Fallback LLM | Claude 3.5 Sonnet | latest | Fallback / tareas complejas | ~$0.003-0.015/token |
| Fallback chain | Hermes nativo | 0.6.0+ | Failover automatico | built-in |
| Local LLM | Ollama / LM Studio | 0.4+ / 0.12.0+ | Desarrollo offline | OSS / Gratis |
| Context compression | Hermes nativo | built-in | Compresion de contexto | built-in |
| Credential pools | Hermes nativo | 0.7.0+ | Rotacion de API keys | built-in |
| Subagentes | Hermes delegate | built-in | Tareas aisladas | built-in |
| API Server | OpenAI-compatible | 0.4.0+ | /v1/chat/completions | built-in |
| TUI | Ink (React) | 0.11.0+ | Interfaz terminal interactiva | built-in |
| Plugins | Python files | 0.3.0+ | Custom tools | built-in |
| MCP | Model Context Protocol | 0.4.0+ | Integracion MCP | built-in |
#### Odoo 17 (ERP)
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| ERP | Odoo Community | 17.0 | Backend negocio completo | LGPL / Gratis |
| Lenguaje | Python | 3.10+ | Logica de servidor | OSS |
| Database | PostgreSQL | 15+ | Datos ERP (compartido con Supabase o separado) | OSS |
| Cache | Redis | 7.x | Cache + sessions + queue | BSD / Gratis |
| Queue | Celery + Redis | 5.x+ | Tareas asincronas | OSS |
#### Frontend SKEEN
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| Framework | React | 18 | UI components | MIT / Gratis |
| Lenguaje | TypeScript | 5.x | Tipado estatico | OSS |
| Build | Vite | 5.x+ | Bundler + dev server | MIT / Gratis |
| Estilos | Tailwind CSS | v3+ | Estilos utility-first | MIT / Gratis |
| Routing | React Router | v6+ | SPA routing | MIT / Gratis |
| HTTP Client | Axios | 1.6+ | Llamadas API | MIT / Gratis |
| Estado | Zustand | 4.x+ | State management | MIT / Gratis |
| Calendario | react-big-calendar | 1.x+ | Vista agenda citas | MIT |
| Charts | Recharts | 2.x+ | Graficos dashboard | MIT |
#### Infraestructura
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| VPS | Hetzner Cloud | CPX31 | 4 vCPU / 8 GB / 160 GB SSD | ~$15/mes |
| OS | Ubuntu Server | 24.04 LTS | Sistema operativo | Gratis |
| Proxy | Nginx | 1.24+ | Reverse proxy + SSL + rate limit | BSD |
| SSL | Let's Encrypt | latest | Certificados HTTPS gratis | Gratis |
| Contenedores | Docker | 25.x+ | Container runtime | Apache 2.0 |
| Orquestacion | Docker Compose | 2.24+ | Multi-container local | Apache 2.0 |
| DNS | Cloudflare | Free tier | DNS + CDN + DDoS protection | Gratis |
| Monitoreo | Uptime Kuma | 1.x+ | Health checks + alerting | MIT |
#### Pagos
| Capa | Tecnologia | Version | Proposito | Costo |
|------|-----------|---------|-----------|-------|
| Pagos intl | Stripe | latest | Tarjetas internacionales | 2.9% + $0.30 |
| Pagos MX | MercadoPago | latest | OXXO, tarjetas MX, SPEI | 3.5-4.5% |
#### Desarrollo
| Capa | Tecnologia | Version | Proposito | Licencia/Costo |
|------|-----------|---------|-----------|---------------|
| Git | GitHub | - | Repositorios + CI/CD | Free/Pro |
| IDE | VS Code | latest | Editor + extensions | Gratis |
| API Testing | Postman / Hoppscotch | latest | Test APIs | Gratis |
| DB Admin | pgAdmin / Tableplus | latest | Admin PostgreSQL | Gratis |
### 3.2 Diagrama de Puertos y Servicios
| Servicio | Puerto Interno | Puerto Externo | Path Nginx |
|----------|---------------|----------------|------------|
| WACRM (Next.js) | 3000 | 443 | /crm/* → localhost:3000 |
| Hermes Gateway | 8080 | 443 | /webhook/* → localhost:8080 |
| Hermes API Server | 8080 (mismo) | - | Solo interno (localhost) |
| Hermes TUI | - | - | Interactivo local |
| Odoo | 8069 | 443 | /odoo/* → localhost:8069 |
| Odoo Longpolling | 8072 | - | Solo interno |
| Frontend React | 5173 (dev) / 80 (prod) | 443 | / → localhost:80 |
| PostgreSQL | 5432 | - | Solo interno |
| Redis | 6379 | - | Solo interno |
| Nginx | 80, 443 | 80, 443 | Reverse proxy principal |
| Uptime Kuma | 3001 | 443 | /status → localhost:3001 |
---
## 4. Estructura de Proyecto Actualizada
### 4.1 Repositorios Git
```
skeen-derma-experts/ (organizacion GitHub)
├── wacrm/ (fork de ArnasDon/wacrm)
│ ├── src/
│ │ ├── app/
│ │ │ ├── (auth)/ # login, signup
│ │ │ ├── (dashboard)/ # UI autenticada
│ │ │ │ ├── dashboard/ # home / metrics
│ │ │ │ ├── inbox/ # shared inbox
│ │ │ │ ├── contacts/ # contactos + tags
│ │ │ │ ├── pipelines/ # Kanban deals
│ │ │ │ ├── broadcasts/ # campañas
│ │ │ │ ├── automations/ # flow builder
│ │ │ │ └── settings/ # perfil / WhatsApp / API keys
│ │ │ ├── api/ # endpoints server-only
│ │ │ │ ├── whatsapp/webhook/ # inbound desde Meta
│ │ │ │ ├── whatsapp/send/ # outbound a Meta
│ │ │ │ ├── automations/ # engine + cron
│ │ │ │ └── v1/ # API publica (messages, contacts, etc.)
│ │ ├── components/
│ │ ├── lib/
│ │ │ ├── supabase/ # client, server, middleware
│ │ │ ├── whatsapp/ # Meta API client
│ │ │ └── automations/ # engine, steps, validation
│ │ └── types/
│ ├── supabase/migrations/ # SQL idempotente
│ ├── .env.local # variables de entorno
│ └── package.json
├── hermes/ # Gateway + Skills SKEEN
│ ├── .hermes/
│ │ ├── config.yaml # config principal
│ │ ├── plugins/
│ │ │ ├── odoo_api_call.py # plugin Odoo XML-RPC
│ │ │ ├── wacrm_send.py # plugin WACRM API
│ │ │ └── calendar_check.py # plugin disponibilidad
│ │ └── workspace/
│ │ ├── AGENTS.md # instrucciones operativas
│ │ ├── SOUL.md # personalidad "Sofia"
│ │ ├── MEMORY.md # memoria persistente
│ │ ├── TOOLS.md # notas de tools
│ │ ├── IDENTITY.md # nombre/vibe/emoji
│ │ └── USER.md # perfil clinica
│ ├── skills/
│ │ ├── skeen-rag/
│ │ │ └── SKILL.md # catalogo servicios
│ │ ├── skeen-agendar/
│ │ │ └── SKILL.md # agendamiento citas
│ │ ├── skeen-pagos/
│ │ │ └── SKILL.md # pagos Stripe/MP
│ │ ├── skeen-monedero/
│ │ │ └── SKILL.md # monedero digital
│ │ └── skeen-recordatorios/
│ │ └── SKILL.md # recordatorios citas
│ ├── webhooks/
│ │ └── wacrm_webhook_server.py # servidor HTTP para webhooks WACRM
│ ├── pyproject.toml
│ ├── requirements.txt
│ └── Dockerfile
├── odoo-backend/ # Odoo 17 + modulos custom
│ ├── odoo/
│ │ ├── addons/
│ │ │ ├── skeen_pacientes/ # gestion pacientes
│ │ │ ├── skeen_citas/ # agendamiento
│ │ │ ├── skeen_monedero/ # monedero digital
│ │ │ ├── skeen_pagos/ # integracion Stripe/MP
│ │ │ ├── skeen_whatsapp/ # webhook WACRM → Odoo
│ │ │ └── skeen_reportes/ # reportes clinicos
│ │ └── odoo.conf
│ ├── Dockerfile
│ └── docker-compose.yml
├── frontend/ # React 18 + TypeScript
│ ├── src/
│ │ ├── components/ # UI components
│ │ ├── pages/ # vistas (Home, Agenda, Pagos)
│ │ ├── hooks/ # custom hooks
│ │ ├── services/ # API clients (Odoo + WACRM)
│ │ ├── store/ # Zustand state
│ │ └── types/ # TypeScript types
│ ├── package.json
│ └── Dockerfile
└── infra/ # Docker Compose completo
├── docker-compose.yml # orquestacion total
├── .env # variables globales
├── nginx/
│ ├── nginx.conf # reverse proxy
│ └── ssl/ # certificados Let's Encrypt
├── scripts/
│ ├── init-supabase.sh # migraciones SQL
│ ├── setup-wacrm.sh # post-install WACRM
│ └── setup-hermes.sh # post-install Hermes
└── monitoring/
├── uptime-kuma/ # health checks
└── prometheus/ # metrics (opcional)
```
### 4.2 Estructura de Docker Compose
```yaml
# infra/docker-compose.yml — Resumen de servicios
services:
# ── WACRM (CRM WhatsApp) ──────────────────────────────
wacrm:
build: ../wacrm/
ports: ["3000:3000"]
environment:
- NEXT_PUBLIC_SUPABASE_URL=${SUPABASE_URL}
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE}
- ENCRYPTION_KEY=${WACRM_ENCRYPTION_KEY}
- WHATSAPP_PHONE_NUMBER_ID=${META_PHONE_ID}
- WHATSAPP_BUSINESS_ACCOUNT_ID=${META_BUSINESS_ID}
- WHATSAPP_ACCESS_TOKEN=${META_ACCESS_TOKEN}
- WHATSAPP_VERIFY_TOKEN=${META_VERIFY_TOKEN}
depends_on: [postgres, redis]
# ── Hermes (Agente IA) ──────────────────────────────
hermes:
build: ../hermes/
ports: ["8080:8080"]
environment:
- HERMES_MODEL_PROVIDER=openai
- HERMES_MODEL=gpt-4o
- OPENAI_API_KEY=${OPENAI_API_KEY}
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- ODOO_URL=http://odoo:8069
- ODOO_DB=skeen
- ODOO_API_KEY=${ODOO_API_KEY}
- WACRM_URL=http://wacrm:3000
- WACRM_API_KEY=${WACRM_API_KEY}
- HERMES_WEBHOOK_PORT=8080
- HERMES_API_SERVER_ENABLED=true
- HERMES_CRON_ENABLED=true
volumes:
- hermes_workspace:/root/.hermes/workspace
- hermes_plugins:/root/.hermes/plugins
- ./../hermes/skills:/skills:ro
depends_on: [odoo, wacrm]
command: ["hermes", "gateway", "start"]
# ── Odoo 17 (ERP) ─────────────────────────────────────
odoo:
build: ../odoo-backend/
ports: ["8069:8069", "8072:8072"]
environment:
- HOST=postgres
- PORT=5432
- USER=odoo
- PASSWORD=${POSTGRES_PASSWORD}
- DB_NAME=skeen
volumes:
- odoo_data:/var/lib/odoo
- ./../odoo-backend/odoo/addons:/mnt/extra-addons:ro
depends_on: [postgres, redis]
# ── Frontend React ────────────────────────────────────
frontend:
build: ../frontend/
ports: ["80:80"]
depends_on: [odoo, wacrm]
# ── PostgreSQL ────────────────────────────────────────
postgres:
image: postgres:15-alpine
ports: ["5432:5432"]
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=skeen
volumes:
- postgres_data:/var/lib/postgresql/data
# ── Redis ─────────────────────────────────────────────
redis:
image: redis:7-alpine
ports: ["6379:6379"]
volumes:
- redis_data:/data
# ── Nginx (Reverse Proxy) ─────────────────────────────
nginx:
image: nginx:1.24-alpine
ports: ["443:443"]
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on: [wacrm, odoo, frontend, hermes]
# ── Uptime Kuma ───────────────────────────────────────
uptime-kuma:
image: louislam/uptime-kuma:1
ports: ["3001:3001"]
volumes:
- uptime_data:/app/data
volumes:
hermes_workspace:
hermes_plugins:
odoo_data:
postgres_data:
redis_data:
uptime_data:
```
### 4.3 Variables de Entorno (.env)
```bash
# ============================================================
# INFRA — infra/.env
# ============================================================
# PostgreSQL
POSTGRES_PASSWORD=skeen_postgres_2026_secure
POSTGRES_USER=odoo
POSTGRES_DB=skeen
# Redis
REDIS_PASSWORD=skeen_redis_2026
# WACRM
SUPABASE_URL=http://postgres:5432
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE=eyJ...
WACRM_ENCRYPTION_KEY=a1b2c3d4e5f6... # 64 chars hex AES-256
META_PHONE_ID=123456789012345
META_BUSINESS_ID=987654321098765
META_ACCESS_TOKEN=EAA...
META_VERIFY_TOKEN=skeen_webhook_verify_2026
# Hermes
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
HERMES_MODEL_PROVIDER=openai
HERMES_MODEL=gpt-4o
HERMES_FALLBACK_PROVIDERS=anthropic,openrouter
HERMES_API_SERVER_ENABLED=true
HERMES_CRON_ENABLED=true
HERMES_WEBHOOK_PORT=8080
HERMES_TUI_ENABLED=false
ODOO_API_KEY=skeen_odoo_api_key_2026
WACRM_API_KEY=wacrm_live_xxxxxxxx
# Stripe / MercadoPago
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
MP_ACCESS_TOKEN=TEST-...
MP_PUBLIC_KEY=TEST-...
# Dominio
DOMAIN=skeen.mx
WACRM_SUBDOMAIN=crm.skeen.mx
ODOO_SUBDOMAIN=erp.skeen.mx
APP_SUBDOMAIN=app.skeen.mx
HERMES_SUBDOMAIN=hermes.skeen.mx
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
# Configuracion principal de Hermes para SKEEN Derma Experts
# Guardar en: ~/.hermes/config.yaml
# ── Perfil ──────────────────────────────────────────────────────
profile: skeen
# ── Modelo y Providers ─────────────────────────────────────────
model:
provider: openai
primary: gpt-4o
fallbacks:
- anthropic:claude-3-5-sonnet
- openrouter:openai/gpt-4o
# Pools de credenciales (rotacion automatica)
credentials:
openai:
- key: ${OPENAI_API_KEY}
label: primary
- key: ${OPENAI_API_KEY_BACKUP}
label: backup
anthropic:
- key: ${ANTHROPIC_API_KEY}
label: primary
# ── Gateway ──────────────────────────────────────────────────────
gateway:
enabled: true
port: 8080
bind: 0.0.0.0
# Plataformas habilitadas (WACRM es el canal principal)
platforms:
webhook:
enabled: true
path: /webhook/wacrm
secret: ${WACRM_WEBHOOK_SECRET}
# WhatsApp directo via Baileys (opcional, fallback)
whatsapp:
enabled: false # Usamos WACRM como gateway
# API Server OpenAI-compatible
api_server:
enabled: true
port: 8080
cors_origins:
- https://crm.skeen.mx
- https://app.skeen.mx
# ── Cron ─────────────────────────────────────────────────────────
cron:
enabled: true
jobs:
# Recordatorios de citas (24h antes)
- name: recordatorios-citas
schedule: "0 18 * * *" # 6 PM diario
command: "skill skeen-recordatorios --action send"
# Generar recordatorios nuevos
- name: generar-recordatorios
schedule: "0 */6 * * *" # Cada 6 horas
command: "skill skeen-recordatorios --action generate"
# Follow-ups post-tratamiento
- name: followups-semanal
schedule: "0 10 * * 1" # Lunes 10 AM
command: "skill skeen-recordatorios --action followup"
# Re-engagement pacientes inactivos
- name: reengagement-semanal
schedule: "0 9 * * 1" # Lunes 9 AM
command: "skill skeen-recordatorios --action reengage"
# ── Memoria ──────────────────────────────────────────────────────
memory:
provider: sqlite # built-in
sqlite_path: ~/.hermes/workspace/memory.db
# Archivos de memoria persistente
files:
- ~/.hermes/workspace/MEMORY.md
- ~/.hermes/workspace/SOUL.md
- ~/.hermes/workspace/AGENTS.md
# ── Plugins ────────────────────────────────────────────────────
plugins:
enabled: true
paths:
- ~/.hermes/plugins
# Plugins custom de SKEEN
custom:
- name: odoo_api_call
file: odoo_api_call.py
enabled: true
- name: wacrm_send
file: wacrm_send.py
enabled: true
# ── Skills ───────────────────────────────────────────────────────
skills:
enabled: true
paths:
- ~/.hermes/skills
- /skills # Docker volume
# Skills de SKEEN
active:
- skeen-rag
- skeen-agendar
- skeen-pagos
- skeen-monedero
- skeen-recordatorios
# ── Tools ────────────────────────────────────────────────────────
tools:
allowed:
- web_search
- web_fetch
- exec
- shell
- file_read
- file_write
- browser
# Overrides de tools built-in
overrides:
- tool: exec
sandbox: docker
# ── Seguridad ────────────────────────────────────────────────────
security:
redact_secrets: true
redact_pii: true
# Aprobaciones
approvals:
mode: smart # smart, always, never
remember_duration: 3600 # segundos
# ── Logging ─────────────────────────────────────────────────────
logging:
level: info
format: json
file: ~/.hermes/logs/hermes.log
# ── Compresion de Contexto ──────────────────────────────────────
compression:
enabled: true
threshold: 0.5 # 50% de la ventana de contexto
protect_last_n: 20 # Proteger ultimos 20 mensajes
# ── Variables de Entorno ─────────────────────────────────────────
# Estas se sobreescriben con variables de entorno del sistema
env:
ODOO_URL: http://odoo:8069
ODOO_DB: skeen
WACRM_URL: http://wacrm:3000

View File

@@ -0,0 +1,108 @@
import os
import xmlrpc.client
from typing import Any, Dict, List, Optional
ODOO_URL = os.environ.get("ODOO_URL", "http://odoo:8069")
ODOO_DB = os.environ.get("ODOO_DB", "skeen")
ODOO_API_KEY = os.environ.get("ODOO_API_KEY")
def odoo_api_call(model: str, method: str, args: List = None, kwargs: Dict = None) -> Any:
"""
Plugin de Hermes para comunicarse con Odoo via XML-RPC.
Uso desde skills:
odoo_api_call(model="salon.booking", method="get_available_slots",
args=[service_id, date_from, date_to])
Args:
model: Nombre del modelo Odoo (ej: 'res.partner', 'salon.booking')
method: Metodo a ejecutar (ej: 'search_read', 'create', 'write')
args: Lista de argumentos posicionales
kwargs: Diccionario de argumentos nombrados
Returns:
Resultado de la llamada XML-RPC
"""
if args is None:
args = []
if kwargs is None:
kwargs = {}
if not ODOO_API_KEY:
raise ValueError("ODOO_API_KEY no configurada en variables de entorno")
# Autenticacion
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, 1, ODOO_API_KEY, {})
if not uid:
raise ValueError("Autenticacion con Odoo fallida")
# Ejecutar metodo
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
result = models.execute_kw(
ODOO_DB, uid, ODOO_API_KEY,
model, method, args, kwargs
)
return result
def odoo_search_read(model: str, domain: List = None, fields: List[str] = None,
limit: int = 100) -> List[Dict]:
"""
Atajo para search_read en Odoo.
Uso:
odoo_search_read(model="res.partner",
domain=[["phone", "=", "+526641234567"]],
fields=["name", "email", "phone"])
"""
if domain is None:
domain = []
if fields is None:
fields = []
return odoo_api_call(
model=model,
method="search_read",
args=[domain, fields],
kwargs={"limit": limit}
)
def odoo_create(model: str, values: Dict) -> int:
"""
Atajo para create en Odoo.
Uso:
odoo_create(model="res.partner",
values={"name": "Sofia Garcia", "phone": "+526641234567"})
"""
return odoo_api_call(model=model, method="create", args=[values])
def odoo_write(model: str, ids: List[int], values: Dict) -> bool:
"""
Atajo para write en Odoo.
Uso:
odoo_write(model="salon.booking", ids=[123],
values={"state": "cancelled"})
"""
return odoo_api_call(model=model, method="write", args=[ids, values])
# Registro del plugin para Hermes
PLUGIN_NAME = "odoo_api_call"
PLUGIN_VERSION = "1.0.0"
PLUGIN_DESCRIPTION = "Comunicacion con Odoo ERP via XML-RPC"
# Herramientas expuestas (para hermes tools)
TOOLS = {
"odoo_api_call": odoo_api_call,
"odoo_search_read": odoo_search_read,
"odoo_create": odoo_create,
"odoo_write": odoo_write,
}

View File

@@ -0,0 +1,102 @@
import os
import json
import requests
from typing import Dict, Optional, Any
WACRM_URL = os.environ.get("WACRM_URL", "http://wacrm:3000")
WACRM_API_KEY = os.environ.get("WACRM_API_KEY")
def wacrm_send(to: str, type: str = "text", text: str = None,
template: Dict = None, media_url: str = None) -> Dict[str, Any]:
"""
Plugin de Hermes para enviar mensajes via WACRM API.
Uso desde skills:
wacrm_send(to="+526641234567", type="text",
text="Hola! Tu cita esta confirmada")
Args:
to: Numero de telefono destino (formato E.164: +52664...)
type: Tipo de mensaje: 'text', 'template', 'image', 'document'
text: Contenido del mensaje (para type='text')
template: Dict con {name, language, params} (para type='template')
media_url: URL del archivo (para type='image' o 'document')
Returns:
Dict con la respuesta de WACRM API
"""
if not WACRM_API_KEY:
raise ValueError("WACRM_API_KEY no configurada en variables de entorno")
payload = {"to": to, "type": type}
if type == "text" and text:
payload["text"] = text
elif type == "template" and template:
payload["template"] = template
elif type in ("image", "document") and media_url:
payload["media_url"] = media_url
response = requests.post(
f"{WACRM_URL}/api/v1/messages",
headers={
"Authorization": f"Bearer {WACRM_API_KEY}",
"Content-Type": "application/json"
},
json=payload,
timeout=30
)
if not response.ok:
error_data = response.json() if response.content else {}
raise Exception(f"WACRM send failed: {error_data.get('error', {}).get('message', response.status_text)}")
return response.json()
def wacrm_send_template(to: str, template_name: str, language: str = "es_MX",
params: list = None) -> Dict[str, Any]:
"""
Atajo para enviar templates de WhatsApp aprobados.
Uso:
wacrm_send_template(to="+526641234567",
template_name="recordatorio_cita_skeen",
params=["Sofia", "5 de julio", "10:00 AM"])
"""
if params is None:
params = []
return wacrm_send(
to=to,
type="template",
template={
"name": template_name,
"language": language,
"params": params
}
)
def wacrm_send_text(to: str, text: str) -> Dict[str, Any]:
"""
Atajo para enviar mensaje de texto simple.
Uso:
wacrm_send_text(to="+526641234567", text="Hola! Como estas?")
"""
return wacrm_send(to=to, type="text", text=text)
# Registro del plugin para Hermes
PLUGIN_NAME = "wacrm_send"
PLUGIN_VERSION = "1.0.0"
PLUGIN_DESCRIPTION = "Envio de mensajes WhatsApp via WACRM API"
# Herramientas expuestas
TOOLS = {
"wacrm_send": wacrm_send,
"wacrm_send_template": wacrm_send_template,
"wacrm_send_text": wacrm_send_text,
}

View File

@@ -0,0 +1,60 @@
# Instrucciones Operativas — SKEEN
## Flujos de trabajo
1. **Agendar Cita:** Cargar skill `skeen-agendar` → Odoo slots → WACRM confirm → Programar cron recordatorio
2. **Consultar Catalogo:** Cargar skill `skeen-rag` → Catalogo servicios → Ofrecer valoracion
3. **Procesar Pago:** Cargar skill `skeen-pagos` → Odoo saldo → Stripe/MP link → Confirmar
4. **Consultar Monedero:** Cargar skill `skeen-monedero` → Odoo balance → Historial
5. **Recordatorios (Cron):** `skeen-recordatorios` ejecuta via `hermes cron` cada 6h/18h
6. **Follow-ups:** `skeen-recordatorios` —action followup cada lunes 10 AM
7. **Re-engagement:** `skeen-recordatorios` —action reengage cada lunes 9 AM
## Reglas de seguridad
- NUNCA compartir API keys en mensajes
- NUNCA ejecutar comandos destructivos en produccion
- SIEMPRE verificar identidad del paciente por telefono
- Limitar redencion de puntos a 50% del total
- Validar todos los inputs antes de ejecutar en Odoo
## Comandos de Hermes utiles
```bash
# Ver estado del gateway
hermes gateway status
# Ver logs
hermes logs --follow
# Diagnosticar configuracion
hermes doctor
# Listar cron jobs activos
hermes cron list
# Ejecutar skill manualmente
hermes skill skeen-agendar --phone +526641234567
# Ver memoria
hermes memory read
# Comprimir contexto
hermes compress
# Cambiar modelo
hermes model
# Actualizar
hermes update
```
## Integraciones
| Sistema | Protocolo | Endpoint | Auth |
|---------|-----------|----------|------|
| WACRM | HTTPS REST | http://wacrm:3000/api/v1 | Bearer API Key |
| Odoo | XML-RPC | http://odoo:8069/xmlrpc/2 | API Key |
| Stripe | HTTPS REST | https://api.stripe.com | Secret Key |
| MercadoPago | HTTPS REST | https://api.mercadopago.com | Access Token |
| Meta API | HTTPS REST | https://graph.facebook.com/v18.0 | Access Token |

View File

@@ -0,0 +1,45 @@
# Sofia — Asistente Virtual de SKEEN Derma Experts
## Identidad
- **Nombre:** Sofia
- **Rol:** Asistente virtual de SKEEN Derma Experts
- **Personalidad:** Calida, profesional, empatica, eficiente
- **Idioma:** Espanol (Mexico)
- **Emoji favorito:** 💙✨🌸
## Tonos de voz
- **Saludo:** "Hola! Soy Sofia de SKEEN 💙"
- **Confirmacion:** "Perfecto! Tu cita esta confirmada ✨"
- **Empatia:** "Entiendo, estamos aqui para cuidar tu piel 🌸"
- **Urgencia:** "Voy a conectarte con un especialista de inmediato"
## Limites
- NO diagnosticar condiciones medicas
- NO recomendar medicamentos especificos
- SIEMPRE sugerir consulta presencial para evaluaciones
- Max 3 parrafos por mensaje de WhatsApp
- Max 2 emojis por mensaje
## Datos de la Clinica
- **Nombre:** SKEEN Derma Experts, Playas de Rosarito, BC
- **Horario:** Lun-Vie 9:00-18:00, Sab 9:00-14:00
- **Direccion:** Blvd. Popotla 123, Playas de Rosarito
- **Telefono:** +52 (664) 123-4567
## Handoff a Humano
**Trigger:** "AGENTE", "HUMANO", "ESPECIALISTA", "DOCTOR"
**Respuesta:** "Te conecto con un especialista enseguida. Espera un momento 💙"
## Contexto de Hermes
- Sofia es el agente principal del perfil `skeen` en Hermes
- Usa el modelo `gpt-4o` como primario, con fallback a Claude 3.5 Sonnet
- Su memoria persiste en SQLite + archivos MEMORY.md
- Puede delegar tareas a subagentes para tareas complejas
- Se comunica con pacientes via WACRM (WhatsApp)

33
hermes/hermes/Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
# Dockerfile para Hermes (Python)
FROM python:3.12-slim
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
git \
curl \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Instalar Hermes
RUN pip install --no-cache-dir "hermes-agent[messaging,cron,web]"
# Instalar dependencias adicionales
RUN pip install --no-cache-dir \
flask \
requests \
xmlrpc.client
# Copiar plugins y skills
COPY hermes/plugins/ /root/.hermes/plugins/
COPY hermes/skills/ /skills/
COPY hermes/workspace/ /root/.hermes/workspace/
COPY hermes/config.yaml /root/.hermes/config.yaml
COPY hermes/webhooks/ /app/webhooks/
# Exponer puertos
EXPOSE 8080
# Comando de inicio
CMD ["hermes", "gateway", "start"]

159
hermes/hermes/config.yaml Normal file
View File

@@ -0,0 +1,159 @@
# Configuracion principal de Hermes para SKEEN Derma Experts
# Guardar en: ~/.hermes/config.yaml
# ── Perfil ──────────────────────────────────────────────────────
profile: skeen
# ── Modelo y Providers ─────────────────────────────────────────
model:
provider: openai
primary: gpt-4o
fallbacks:
- anthropic:claude-3-5-sonnet
- openrouter:openai/gpt-4o
# Pools de credenciales (rotacion automatica)
credentials:
openai:
- key: ${OPENAI_API_KEY}
label: primary
- key: ${OPENAI_API_KEY_BACKUP}
label: backup
anthropic:
- key: ${ANTHROPIC_API_KEY}
label: primary
# ── Gateway ──────────────────────────────────────────────────────
gateway:
enabled: true
port: 8080
bind: 0.0.0.0
# Plataformas habilitadas (WACRM es el canal principal)
platforms:
webhook:
enabled: true
path: /webhook/wacrm
secret: ${WACRM_WEBHOOK_SECRET}
# WhatsApp directo via Baileys (opcional, fallback)
whatsapp:
enabled: false # Usamos WACRM como gateway
# API Server OpenAI-compatible
api_server:
enabled: true
port: 8080
cors_origins:
- https://crm.skeen.mx
- https://app.skeen.mx
# ── Cron ─────────────────────────────────────────────────────────
cron:
enabled: true
jobs:
# Recordatorios de citas (24h antes)
- name: recordatorios-citas
schedule: "0 18 * * *" # 6 PM diario
command: "skill skeen-recordatorios --action send"
# Generar recordatorios nuevos
- name: generar-recordatorios
schedule: "0 */6 * * *" # Cada 6 horas
command: "skill skeen-recordatorios --action generate"
# Follow-ups post-tratamiento
- name: followups-semanal
schedule: "0 10 * * 1" # Lunes 10 AM
command: "skill skeen-recordatorios --action followup"
# Re-engagement pacientes inactivos
- name: reengagement-semanal
schedule: "0 9 * * 1" # Lunes 9 AM
command: "skill skeen-recordatorios --action reengage"
# ── Memoria ──────────────────────────────────────────────────────
memory:
provider: sqlite # built-in
sqlite_path: ~/.hermes/workspace/memory.db
# Archivos de memoria persistente
files:
- ~/.hermes/workspace/MEMORY.md
- ~/.hermes/workspace/SOUL.md
- ~/.hermes/workspace/AGENTS.md
# ── Plugins ────────────────────────────────────────────────────
plugins:
enabled: true
paths:
- ~/.hermes/plugins
# Plugins custom de SKEEN
custom:
- name: odoo_api_call
file: odoo_api_call.py
enabled: true
- name: wacrm_send
file: wacrm_send.py
enabled: true
# ── Skills ───────────────────────────────────────────────────────
skills:
enabled: true
paths:
- ~/.hermes/skills
- /skills # Docker volume
# Skills de SKEEN
active:
- skeen-rag
- skeen-agendar
- skeen-pagos
- skeen-monedero
- skeen-recordatorios
# ── Tools ────────────────────────────────────────────────────────
tools:
allowed:
- web_search
- web_fetch
- exec
- shell
- file_read
- file_write
- browser
# Overrides de tools built-in
overrides:
- tool: exec
sandbox: docker
# ── Seguridad ────────────────────────────────────────────────────
security:
redact_secrets: true
redact_pii: true
# Aprobaciones
approvals:
mode: smart # smart, always, never
remember_duration: 3600 # segundos
# ── Logging ─────────────────────────────────────────────────────
logging:
level: info
format: json
file: ~/.hermes/logs/hermes.log
# ── Compresion de Contexto ──────────────────────────────────────
compression:
enabled: true
threshold: 0.5 # 50% de la ventana de contexto
protect_last_n: 20 # Proteger ultimos 20 mensajes
# ── Variables de Entorno ─────────────────────────────────────────
# Estas se sobreescriben con variables de entorno del sistema
env:
ODOO_URL: http://odoo:8069
ODOO_DB: skeen
WACRM_URL: http://wacrm:3000

View File

@@ -0,0 +1,112 @@
{
"phone": "+5216642170990",
"context": {},
"history": [
{
"role": "user",
"content": "me gustaria agendar",
"ts": "2026-07-10T07:27:02.435Z"
},
{
"role": "assistant",
"content": "Claro, con gusto te ayudo a agendar. ¿Qué servicio necesitas?\n\n• Consulta Dermatologica General — $800 MXN (30 min)\n• Tratamiento Facial para Acne — $1500 MXN (60 min)\n• Peel Quimico — $1500 MXN (30 min)\n• Depilacion Laser — $500 MXN (15 min)\n• Tratamiento Anti-Edad — $2500 MXN (75 min)\n\nTambién puedes pedir una **consulta de valoración gratuita** (15 min).",
"ts": "2026-07-10T07:27:03.426Z"
},
{
"role": "user",
"content": "Consulta Dermatologica General",
"ts": "2026-07-10T07:27:15.760Z"
},
{
"role": "assistant",
"content": "Perfecto, elegiste **Consulta Dermatologica General** (30 min).\n\n¿Qué día te gustaría agendar? Puedes escribir la fecha como \"mañana\", \"viernes\", o \"2026-07-15\".",
"ts": "2026-07-10T07:27:16.602Z"
},
{
"role": "user",
"content": "el martes que viene",
"ts": "2026-07-10T07:27:28.665Z"
},
{
"role": "user",
"content": "hola",
"ts": "2026-07-10T07:45:21.611Z"
},
{
"role": "assistant",
"content": "¡Hola! 👋 ¿Cómo estás?\n\nPara continuar con tu cita de **Consulta Dermatológica General** el **martes que viene**, ¿a qué hora te gustaría asistir? Tenemos disponibilidad de 9:00 a 18:00 de lunes a viernes.\n\nQuedo atenta a tu respuesta para confirmar los detalles. 😊",
"ts": "2026-07-10T07:45:27.127Z"
},
{
"role": "user",
"content": "a las 9 esta bien",
"ts": "2026-07-10T07:45:47.333Z"
},
{
"role": "assistant",
"content": "Claro, con gusto te ayudo a agendar. ¿Qué servicio necesitas?\n\n• ANESTESIOLOGA NO SHOW — $2000 MXN (30 min)\n• ANESTESIOLOGA SESION — $4000 MXN (30 min)\n• ANTICIPO CREMA HORMONAL — $1000 MXN (30 min)\n• Acné 1ra vez — $900 MXN (30 min)\n• Asesoria skincare — $600 MXN (30 min)\n• BIOESTIMULACION ADN SALMON — $3500 MXN (30 min)\n\nSi no sabes cuál elegir, agenda una **consulta** y te orientamos.",
"ts": "2026-07-10T07:45:50.146Z"
},
{
"role": "user",
"content": "*consulta*",
"ts": "2026-07-10T07:46:07.555Z"
},
{
"role": "assistant",
"content": "Perfecto, elegiste **Consulta** (30 min).\n\n¿Qué día te gustaría agendar? Puedes escribir la fecha como \"mañana\", \"viernes\", o \"2026-07-15\".",
"ts": "2026-07-10T07:46:08.469Z"
},
{
"role": "user",
"content": "el martes que viene",
"ts": "2026-07-10T07:46:17.501Z"
},
{
"role": "assistant",
"content": "Estos son los horarios disponibles para **Consulta** el **martes, 14 de julio de 2026**:\n\n1. 9:00 AM\n2. 9:30 AM\n3. 10:00 AM\n4. 10:30 AM\n5. 11:00 AM\n\nEscribe el número o la hora que prefieras.",
"ts": "2026-07-10T07:46:18.513Z"
},
{
"role": "user",
"content": "a las 9 am esta bien",
"ts": "2026-07-10T07:46:29.373Z"
},
{
"role": "assistant",
"content": "No entendí el horario. Por favor elige una opción de la lista (escribe el número o la hora, ej. 10:00). Puedes escribir \"cancelar\" para salir.",
"ts": "2026-07-10T07:46:30.394Z"
},
{
"role": "user",
"content": "1. 9:00 AM",
"ts": "2026-07-10T07:46:41.467Z"
},
{
"role": "assistant",
"content": "Muy bien, horario seleccionado: **9:00 AM**.\n\n¿A qué nombre registramos la cita?",
"ts": "2026-07-10T07:46:42.537Z"
},
{
"role": "user",
"content": "Ivan Alcaraz",
"ts": "2026-07-10T07:46:51.451Z"
},
{
"role": "assistant",
"content": "Confirmemos tu cita:\n\n• Servicio: Consulta\n• Fecha: martes, 14 de julio de 2026\n• Hora: 9:00 AM\n• Paciente: Ivan Alcaraz\n• Teléfono: +5216642170990\n\n¿Confirmamos? (sí/no)",
"ts": "2026-07-10T07:46:52.253Z"
},
{
"role": "user",
"content": "Si",
"ts": "2026-07-10T07:46:58.143Z"
},
{
"role": "assistant",
"content": "¡Perfecto! Tu cita está confirmada ✨\n\n• Servicio: Consulta\n• Fecha: martes, 14 de julio de 2026\n• Hora: 9:00 AM\n• Referencia: CITA-001\n\nDirección: Blvd. Popotla 123, Playas de Rosarito, BC, México\n\nTe enviaremos un recordatorio un día antes. Si necesitas cambiar o cancelar, solo escríbenos.",
"ts": "2026-07-10T07:46:59.258Z"
}
],
"created_at": "2026-07-10T07:26:38.589Z"
}

View File

@@ -0,0 +1,52 @@
{
"reengagement:526618504866:2026-07-09": "2026-07-09",
"reengagement:526611121459:2026-07-09": "2026-07-09",
"reengagement:523312688280:2026-07-09": "2026-07-09",
"reengagement:526645029513:2026-07-09": "2026-07-09",
"reengagement:522094930605:2026-07-09": "2026-07-09",
"reengagement:526611077906:2026-07-09": "2026-07-09",
"reengagement:526194197072:2026-07-09": "2026-07-09",
"reengagement:526653164624:2026-07-09": "2026-07-09",
"reengagement:526193955418:2026-07-09": "2026-07-09",
"reengagement:526634404451:2026-07-09": "2026-07-09",
"reengagement:526461164445:2026-07-09": "2026-07-09",
"reengagement:526196225741:2026-07-09": "2026-07-09",
"reengagement:526611288180:2026-07-09": "2026-07-09",
"reengagement:526642767839:2026-07-09": "2026-07-09",
"reengagement:526197211181:2026-07-09": "2026-07-09",
"reengagement:16197889299:2026-07-09": "2026-07-09",
"reengagement:526643979843:2026-07-09": "2026-07-09",
"reengagement:526611167403:2026-07-09": "2026-07-09",
"reengagement:526192745622:2026-07-09": "2026-07-09",
"reengagement:526197480886:2026-07-09": "2026-07-09",
"reengagement:16197699175:2026-07-09": "2026-07-09",
"reengagement:526643757327:2026-07-09": "2026-07-09",
"reengagement:526618501119:2026-07-09": "2026-07-09",
"reengagement:526642324280:2026-07-09": "2026-07-09",
"reengagement:526198794022:2026-07-09": "2026-07-09",
"reengagement:526641798892:2026-07-09": "2026-07-09",
"reengagement:526611035281:2026-07-09": "2026-07-09",
"reengagement:526648298490:2026-07-09": "2026-07-09",
"reengagement:526641084869:2026-07-09": "2026-07-09",
"reengagement:526643423016:2026-07-09": "2026-07-09",
"reengagement:526612426301:2026-07-09": "2026-07-09",
"reengagement:526631278823:2026-07-09": "2026-07-09",
"reengagement:526611269713:2026-07-09": "2026-07-09",
"reengagement:527143128702:2026-07-09": "2026-07-09",
"reengagement:526192105498:2026-07-09": "2026-07-09",
"reengagement:526645612760:2026-07-09": "2026-07-09",
"reengagement:526642947763:2026-07-09": "2026-07-09",
"reengagement:522431306950:2026-07-09": "2026-07-09",
"reengagement:526642572433:2026-07-09": "2026-07-09",
"reengagement:526646574771:2026-07-09": "2026-07-09",
"reengagement:526611100452:2026-07-09": "2026-07-09",
"reengagement:526642918451:2026-07-09": "2026-07-09",
"reengagement:526611072980:2026-07-09": "2026-07-09",
"reengagement:526644063990:2026-07-09": "2026-07-09",
"reengagement:526642517280:2026-07-09": "2026-07-09",
"reengagement:526612814494:2026-07-09": "2026-07-09",
"reengagement:526647339185:2026-07-09": "2026-07-09",
"reengagement:526861169131:2026-07-09": "2026-07-09",
"reengagement:526616115762:2026-07-09": "2026-07-09",
"reengagement:526641368921:2026-07-09": "2026-07-09"
}

905
hermes/hermes/package-lock.json generated Normal file
View File

@@ -0,0 +1,905 @@
{
"name": "hermes",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hermes",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"node-fetch": "^2.7.0"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^2.0.0",
"debug": "^4.4.3",
"http-errors": "^2.0.1",
"iconv-lite": "^0.7.2",
"on-finished": "^2.4.1",
"qs": "^6.15.2",
"raw-body": "^3.0.2",
"type-is": "^2.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
"license": "BSD-3-Clause",
"dependencies": {
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/raw-body": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/serve-static": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4",
"side-channel-list": "^1.0.1",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/type-is": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
"license": "MIT",
"dependencies": {
"content-type": "^2.0.0",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/type-is/node_modules/content-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "hermes",
"version": "1.0.0",
"description": "",
"main": "webhook-bridge.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^5.2.1",
"node-fetch": "^2.7.0"
}
}

View File

@@ -0,0 +1,108 @@
import os
import xmlrpc.client
from typing import Any, Dict, List, Optional
ODOO_URL = os.environ.get("ODOO_URL", "http://odoo:8069")
ODOO_DB = os.environ.get("ODOO_DB", "skeen")
ODOO_API_KEY = os.environ.get("ODOO_API_KEY")
def odoo_api_call(model: str, method: str, args: List = None, kwargs: Dict = None) -> Any:
"""
Plugin de Hermes para comunicarse con Odoo via XML-RPC.
Uso desde skills:
odoo_api_call(model="salon.booking", method="get_available_slots",
args=[service_id, date_from, date_to])
Args:
model: Nombre del modelo Odoo (ej: 'res.partner', 'salon.booking')
method: Metodo a ejecutar (ej: 'search_read', 'create', 'write')
args: Lista de argumentos posicionales
kwargs: Diccionario de argumentos nombrados
Returns:
Resultado de la llamada XML-RPC
"""
if args is None:
args = []
if kwargs is None:
kwargs = {}
if not ODOO_API_KEY:
raise ValueError("ODOO_API_KEY no configurada en variables de entorno")
# Autenticacion
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
uid = common.authenticate(ODOO_DB, 1, ODOO_API_KEY, {})
if not uid:
raise ValueError("Autenticacion con Odoo fallida")
# Ejecutar metodo
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
result = models.execute_kw(
ODOO_DB, uid, ODOO_API_KEY,
model, method, args, kwargs
)
return result
def odoo_search_read(model: str, domain: List = None, fields: List[str] = None,
limit: int = 100) -> List[Dict]:
"""
Atajo para search_read en Odoo.
Uso:
odoo_search_read(model="res.partner",
domain=[["phone", "=", "+526641234567"]],
fields=["name", "email", "phone"])
"""
if domain is None:
domain = []
if fields is None:
fields = []
return odoo_api_call(
model=model,
method="search_read",
args=[domain, fields],
kwargs={"limit": limit}
)
def odoo_create(model: str, values: Dict) -> int:
"""
Atajo para create en Odoo.
Uso:
odoo_create(model="res.partner",
values={"name": "Sofia Garcia", "phone": "+526641234567"})
"""
return odoo_api_call(model=model, method="create", args=[values])
def odoo_write(model: str, ids: List[int], values: Dict) -> bool:
"""
Atajo para write en Odoo.
Uso:
odoo_write(model="salon.booking", ids=[123],
values={"state": "cancelled"})
"""
return odoo_api_call(model=model, method="write", args=[ids, values])
# Registro del plugin para Hermes
PLUGIN_NAME = "odoo_api_call"
PLUGIN_VERSION = "1.0.0"
PLUGIN_DESCRIPTION = "Comunicacion con Odoo ERP via XML-RPC"
# Herramientas expuestas (para hermes tools)
TOOLS = {
"odoo_api_call": odoo_api_call,
"odoo_search_read": odoo_search_read,
"odoo_create": odoo_create,
"odoo_write": odoo_write,
}

View File

@@ -0,0 +1,102 @@
import os
import json
import requests
from typing import Dict, Optional, Any
WACRM_URL = os.environ.get("WACRM_URL", "http://wacrm:3000")
WACRM_API_KEY = os.environ.get("WACRM_API_KEY")
def wacrm_send(to: str, type: str = "text", text: str = None,
template: Dict = None, media_url: str = None) -> Dict[str, Any]:
"""
Plugin de Hermes para enviar mensajes via WACRM API.
Uso desde skills:
wacrm_send(to="+526641234567", type="text",
text="Hola! Tu cita esta confirmada")
Args:
to: Numero de telefono destino (formato E.164: +52664...)
type: Tipo de mensaje: 'text', 'template', 'image', 'document'
text: Contenido del mensaje (para type='text')
template: Dict con {name, language, params} (para type='template')
media_url: URL del archivo (para type='image' o 'document')
Returns:
Dict con la respuesta de WACRM API
"""
if not WACRM_API_KEY:
raise ValueError("WACRM_API_KEY no configurada en variables de entorno")
payload = {"to": to, "type": type}
if type == "text" and text:
payload["text"] = text
elif type == "template" and template:
payload["template"] = template
elif type in ("image", "document") and media_url:
payload["media_url"] = media_url
response = requests.post(
f"{WACRM_URL}/api/v1/messages",
headers={
"Authorization": f"Bearer {WACRM_API_KEY}",
"Content-Type": "application/json"
},
json=payload,
timeout=30
)
if not response.ok:
error_data = response.json() if response.content else {}
raise Exception(f"WACRM send failed: {error_data.get('error', {}).get('message', response.status_text)}")
return response.json()
def wacrm_send_template(to: str, template_name: str, language: str = "es_MX",
params: list = None) -> Dict[str, Any]:
"""
Atajo para enviar templates de WhatsApp aprobados.
Uso:
wacrm_send_template(to="+526641234567",
template_name="recordatorio_cita_skeen",
params=["Sofia", "5 de julio", "10:00 AM"])
"""
if params is None:
params = []
return wacrm_send(
to=to,
type="template",
template={
"name": template_name,
"language": language,
"params": params
}
)
def wacrm_send_text(to: str, text: str) -> Dict[str, Any]:
"""
Atajo para enviar mensaje de texto simple.
Uso:
wacrm_send_text(to="+526641234567", text="Hola! Como estas?")
"""
return wacrm_send(to=to, type="text", text=text)
# Registro del plugin para Hermes
PLUGIN_NAME = "wacrm_send"
PLUGIN_VERSION = "1.0.0"
PLUGIN_DESCRIPTION = "Envio de mensajes WhatsApp via WACRM API"
# Herramientas expuestas
TOOLS = {
"wacrm_send": wacrm_send,
"wacrm_send_template": wacrm_send_template,
"wacrm_send_text": wacrm_send_text,
}

View File

@@ -0,0 +1,103 @@
---
name: skeen-agendar
description: >
Agendamiento de citas para SKEEN Derma Experts.
Consulta disponibilidad, crea citas, reagenda y cancela.
Usar cuando el usuario quiera agendar, reagendar o cancelar una cita.
version: 1.0.0
metadata:
hermes:
requires:
bins: [python]
envVars:
- name: ODOO_URL
required: true
- name: ODOO_DB
required: true
- name: ODOO_API_KEY
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["agendar", "cita", "hora", "reagendar", "cancelar", "disponibilidad"]
---
# SKEEN — Agendamiento de Citas
## Instrucciones de Uso
### PASO 1: Identificar intencion
- "Quiero agendar" / "Necesito una cita" → NUEVA CITA
- "Quiero cambiar mi cita" / "Reagendar" → REAGENDAR
- "Cancelar mi cita" → CANCELAR
- "Tengo una cita para manana?" → CONSULTAR
### PASO 2: Para NUEVA CITA
1. Pregunta que tipo de servicio desea (si no lo especifico):
- Consulta dermatologica general ($800 MXN, 30 min)
- Tratamiento facial ($1,200-$3,500 MXN, 45-90 min)
- Depilacion laser (desde $500 MXN por zona, 15-60 min)
- Peel quimico ($1,500 MXN, 30 min)
- O consulta de valoracion gratuita (15 min)
2. Consulta disponibilidad usando el plugin `odoo_api_call`:
Modelo: salon.booking, metodo: get_available_slots
Argumentos: [service_id, date_from, date_to]
3. Presenta las 3 mejores opciones de horario
4. Cuando el paciente elige, confirma datos:
- Nombre completo
- Confirmar telefono
- Alergias o condiciones especiales (opcional)
5. Crea la cita usando `odoo_api_call`:
Modelo: salon.booking, metodo: create
Datos: { name, phone, service_id, date, time, state: 'confirmed' }
6. Programa recordatorio via cron:
```bash
hermes cron schedule --name "recordatorio-cita-[ID]" \
--at "[fecha_cita] 18:00 - 1 day" \
--command "skill skeen-recordatorios --phone [PHONE] --cita [DATE]"
```
7. Envia confirmacion via WACRM usando `wacrm_send`
### PASO 3: Para REAGENDAR
1. Busca citas del paciente por telefono via `odoo_api_call`
2. Muestra citas activas
3. Pide nueva fecha/hora
4. Consulta disponibilidad
5. Actualiza cita via `odoo_api_call` (model salon.booking, method write)
6. Confirma cambio
### PASO 4: Para CANCELAR
1. Busca citas del paciente por telefono
2. Confirma cual cita cancelar
3. Pide motivo (opcional, para estadisticas)
4. Cancela via `odoo_api_call` (state: 'cancelled')
5. Ofrece reagendar
## Formato de Confirmacion
Perfecto! Tu cita esta confirmada:
Servicio: [Nombre servicio]
Fecha: [Dia de semana], [DD] de [mes] de [AAAA]
Hora: [HH:MM] AM/PM
Duracion: [X] minutos
Direccion: Blvd. Popotla 123, Playas de Rosarito
Te enviaremos un recordatorio un dia antes. Si necesitas cambiar o cancelar, solo escribenos!
## Reglas Importantes
- Solo agendar dentro del horario de la clinica
- Validar que el paciente no tenga citas duplicadas
- Consulta de valoracion gratuita tiene duracion de 15 min
- Si no hay disponibilidad, ofrecer lista de espera
- Guardar TODAS las citas en memoria para recordatorios

View File

@@ -0,0 +1,55 @@
---
name: skeen-monedero
description: >
Monedero digital de fidelidad de SKEEN Derma Experts.
Consulta saldo, redime puntos, muestra historial.
1 punto = $1 MXN. Se acumula 1 punto por cada $10 gastados.
Usar cuando el usuario pregunte por puntos, saldo, o redencion.
version: 1.0.0
metadata:
hermes:
requires:
bins: []
envVars:
- name: ODOO_URL
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["puntos", "monedero", "saldo", "redimir", "fidelidad"]
---
# SKEEN — Monedero Digital
## Consultar saldo
Usa `odoo_api_call`: modelo skeen.monedero, metodo get_balance, args: [phone_number]
Respuesta esperada: { points: 450, equivalent_mxn: 450, history: [...] }
Enviar via WACRM con formato:
Tu monedero SKEEN:
Saldo actual: 450 puntos (= $450 MXN)
Historial reciente:
- 03/07/2026: +120 pts (Pago Tratamiento Acne — $1,200)
- 28/06/2026: +80 pts (Pago Consulta + peel — $800)
- 15/06/2026: +250 pts (Pago Depilacion laser — $2,500)
Puedes redimir tus puntos en tu proxima visita! Cada 10 puntos = $10 MXN de descuento.
## Redimir puntos
1. Verificar que tenga suficientes puntos
2. Consultar monto a pagar de la cita/servicio
3. Calcular descuento (maximo 50% del total)
4. Confirmar redencion
5. Aplicar via `odoo_api_call`: modelo skeen.monedero, metodo redeem
6. Confirmar redencion via WACRM
## Reglas
- Maximo redimible: 50% del total de la compra
- Puntos vencen a los 12 meses de inactividad
- No transferibles entre pacientes
- Se acumulan en pagos confirmados unicamente
- En devoluciones, se restan los puntos correspondientes

View File

@@ -0,0 +1,79 @@
---
name: skeen-pagos
description: >
Gestion de pagos para SKEEN Derma Experts.
Genera links de pago (Stripe/MercadoPago), consulta estados,
y gestiona facturacion. Usar cuando el usuario quiera pagar,
consultar saldo, o ver sus pagos pendientes.
version: 1.0.0
metadata:
hermes:
requires:
bins: [python]
env: [STRIPE_SECRET_KEY, MP_ACCESS_TOKEN]
envVars:
- name: STRIPE_SECRET_KEY
required: true
- name: MP_ACCESS_TOKEN
required: true
- name: ODOO_URL
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["pagar", "saldo", "deuda", "factura", "pago", "stripe", "mercadopago"]
---
# SKEEN — Gestion de Pagos
## Instrucciones de Uso
### PASO 1: Identificar intencion de pago
- "Quiero pagar" → Generar link de pago
- "Cuanto debo?" / "Saldo pendiente" → Consultar deuda
- "Ya pague" → Verificar pago
- "Necesito factura" → Generar factura (CFDI)
### PASO 2: Consultar servicios pendientes
Usa `odoo_api_call` para consultar modelo account.move:
Dominio: [['partner_id.phone', '=', PACIENTE_PHONE], ['state', '=', 'posted'], ['payment_state', '!=', 'paid']]
Campos: ['name', 'amount_residual', 'invoice_date_due']
### PASO 3: Generar link de pago
Para pagos en Mexico (MXN) — usa MercadoPago:
Crear preference con items del servicio
notification_url: https://skeen.mx/webhooks/mercadopago
back_urls: { success: 'https://app.skeen.mx/pagos/exitoso' }
Para pagos internacionales — usa Stripe Checkout:
Crear session con line_items
mode: 'payment'
success_url: 'https://app.skeen.mx/pagos/exitoso'
metadata: { patient_phone: PACIENTE_PHONE }
### PASO 4: Enviar link de pago
Usa `wacrm_send` para enviar mensaje con resumen y link.
Incluir:
- Listado de servicios con precios
- Total a pagar
- Link de pago
- Metodos aceptados (tarjeta, OXXO, SPEI)
- Puntos de monedero que acumulara
### PASO 5: Confirmacion de pago
Cuando el webhook confirme el pago:
1. Registra pago en Odoo via `odoo_api_call`
2. Calcula puntos de monedero (10% del monto)
3. Envia confirmacion + puntos acumulados
## Reglas
- Siempre confirmar el monto antes de generar el link
- Para pagos >$5,000 MXN, ofrecer opcion de meses sin intereses
- Verificar que el telefono coincida con el paciente
- Guardar referencia de pago en memoria

View File

@@ -0,0 +1,59 @@
---
name: skeen-rag
description: >
Catalogo de servicios dermatologicos de SKEEN Derma Experts.
Proporciona informacion sobre tratamientos, precios, duracion y contraindicaciones.
SOLO usar cuando el usuario pregunte por servicios, tratamientos, precios o el catalogo.
version: 1.0.0
metadata:
hermes:
requires:
bins: []
envVars:
- name: SKEEN_SERVICES_JSON
required: false
description: "Path al archivo JSON con catalogo de servicios"
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["servicios", "tratamientos", "precios", "catalogo", "acne", "peel", "laser", "dermatologia"]
---
# SKEEN Derma Experts — Catalogo de Servicios
## Instrucciones de Uso
Cuando un paciente pregunte por tratamientos, servicios, precios o recomendaciones dermatologicas:
1. Lee el archivo de catalogo: `${env:SKEEN_SERVICES_JSON:-/skills/data/skeen-servicios.json}`
2. Filtra los servicios relevantes segun la pregunta
3. Presenta la informacion de forma clara y amigable
4. SIEMPRE incluye: nombre del tratamiento, precio aproximado, duracion, y una breve descripcion
5. NUNCA diagnostiques condiciones medicas — invita al paciente a agendar una consulta
6. Si no encuentras el servicio, ofrece agendar una consulta de valoracion
## Formato de Respuesta
Hola! En SKEEN contamos con estos tratamientos para [condicion]:
1. **[Nombre tratamiento]** — $[precio] MXN
Duracion: [X] minutos
[Descripcion breve]
2. **[Nombre tratamiento]** — $[precio] MXN
...
Te gustaria agendar una consulta de valoracion? Podemos evaluar tu caso especifico y recomendarte el mejor tratamiento.
## Limites
- NO diagnosticar — solo informacion general
- NO recomendar medicamentos especificos
- SIEMPRE sugerir consulta presencial para evaluacion
- Precios son aproximados — pueden variar segun evaluacion
## Datos de la Clinica
- Nombre: SKEEN Derma Experts
- Direccion: Blvd. Popotla 123, Playas de Rosarito, BC, Mexico
- Horario: Lun-Vie 9:00-18:00, Sab 9:00-14:00
- Telefono: +52 (664) 123-4567

View File

@@ -0,0 +1,89 @@
---
name: skeen-recordatorios
description: >
Recordatorios de citas, follow-ups y re-engagement para SKEEN.
Usar PRINCIPALMENTE via cron (hermes cron).
Tambien puede responder a consultas sobre recordatorios.
version: 1.0.0
metadata:
hermes:
always: true
requires:
bins: []
envVars:
- name: ODOO_URL
required: true
- name: WACRM_URL
required: true
- name: WACRM_API_KEY
required: true
platforms: [whatsapp, webchat, cli]
activation: cron
cron:
- schedule: "0 */6 * * *" # Cada 6 horas: generar recordatorios
command: "skill skeen-recordatorios --action generate"
- schedule: "0 18 * * *" # 6 PM diario: enviar recordatorios
command: "skill skeen-recordatorios --action send"
- schedule: "0 9 * * 1" # Lunes 9 AM: re-engagement
command: "skill skeen-recordatorios --action reengage"
---
# SKEEN — Recordatorios y Follow-ups
## Ejecucion via Cron
Esta skill se ejecuta automaticamente via `hermes cron`.
### PASO 1: Generar recordatorios (cada 6 horas)
Recordatorios de citas (24h antes):
Consulta Odoo: salon.booking con date=tomorrow, state=confirmed
Para cada cita, programa cron:
```bash
hermes cron schedule --name "recordatorio-[booking_id]" \
--at "[fecha_cita] 18:00 - 1 day" \
--command "skill skeen-recordatorios --action send --phone [PHONE] --cita [DATE]"
```
Follow-ups post-tratamiento (7 dias despues):
Consulta Odoo: salon.booking con date=7_days_ago, state=done
Programa cron para cada paciente:
```bash
hermes cron schedule --name "followup-[booking_id]" \
--at "[fecha_cita + 7 days] 10:00" \
--command "skill skeen-recordatorios --action followup --phone [PHONE]"
```
Re-engagement (pacientes inactivos 90 dias):
Consulta Odoo: res.partner con last_visit <= 90_days_ago
Programa cron semanal:
```bash
hermes cron schedule --name "reengage-[partner_id]" \
--at "lunes 9:00" \
--command "skill skeen-recordatorios --action reengage --phone [PHONE]"
```
### PASO 2: Enviar recordatorios (6 PM diario)
Para cada recordatorio programado para hoy:
1. Enviar via WACRM usando `wacrm_send` con template aprobado
2. Si es exitoso, marcar como enviado en Odoo
3. Si falla, reintentar en siguiente ejecucion (max 3 intentos)
### PASO 3: Limpieza
Eliminar cron jobs completados con mas de 7 dias:
```bash
hermes cron list --completed | awk '$4 > 7 {print $1}' | xargs hermes cron delete
```
## Templates de WhatsApp Aprobados
recordatorio_cita_skeen:
"Hola {{1}}! Te recordamos tu cita manana {{2}} a las {{3}} en SKEEN Derma Experts."
followup_tratamiento_skeen:
"Hola {{1}}! Han pasado {{2}} dias desde tu {{3}}. Como te sientes?"
reengage_skeen:
"Hola {{1}}! Te extranamos en SKEEN. Agenda tu proxima visita y recibe 50 puntos."

View File

@@ -0,0 +1,98 @@
{
"clinica": {
"nombre": "SKEEN Derma Experts",
"direccion": "Blvd. Popotla 123, Playas de Rosarito, BC, Mexico",
"telefono": "+52 (664) 123-4567",
"horario": {
"lunes_viernes": "9:00 - 18:00",
"sabado": "9:00 - 14:00",
"domingo": "Cerrado"
}
},
"servicios": [
{
"id": "cons-derm",
"nombre": "Consulta Dermatologica",
"categoria": "consulta",
"precio": 800.0,
"duracion_min": 30,
"precio_paquete": 0,
"sesiones_paquete": 0,
"notas_paquete": "",
"descripcion": "Evaluacion completa de la piel con diagnostico y plan de tratamiento personalizado.",
"contraindicaciones": []
},
{
"id": "cons-val",
"nombre": "Consulta de Valoracion",
"categoria": "consulta",
"precio": 0.0,
"duracion_min": 15,
"precio_paquete": 0,
"sesiones_paquete": 0,
"notas_paquete": "",
"descripcion": "Evaluacion breve para recomendar el tratamiento mas adecuado. Sin compromiso.",
"contraindicaciones": []
},
{
"id": "laser-axi",
"nombre": "Depilacion Laser Axila",
"categoria": "tratamiento",
"precio": 500.0,
"duracion_min": 15,
"precio_paquete": 0,
"sesiones_paquete": 0,
"notas_paquete": "",
"descripcion": "Depilacion laser por zona pequena.",
"contraindicaciones": []
},
{
"id": "laser-ful",
"nombre": "Depilacion Laser Full Body",
"categoria": "tratamiento",
"precio": 2500.0,
"duracion_min": 90,
"precio_paquete": 7000.0,
"sesiones_paquete": 3,
"notas_paquete": "Ahorro de $500",
"descripcion": "Depilacion laser full body. Precio por sesion o paquete de 3 sesiones.",
"contraindicaciones": []
},
{
"id": "peel-qui",
"nombre": "Peel Quimico",
"categoria": "tratamiento",
"precio": 1500.0,
"duracion_min": 30,
"precio_paquete": 0,
"sesiones_paquete": 0,
"notas_paquete": "",
"descripcion": "Exfoliacion quimica para renovar la piel tratar manchas y mejorar textura.",
"contraindicaciones": []
},
{
"id": "anti-edad",
"nombre": "Tratamiento Anti-Edad",
"categoria": "tratamiento",
"precio": 2500.0,
"duracion_min": 75,
"precio_paquete": 6500.0,
"sesiones_paquete": 3,
"notas_paquete": "Ahorro de $1000",
"descripcion": "Combinacion de microdermoabrasion vitaminas y mascarilla reafirmante.",
"contraindicaciones": []
},
{
"id": "acne-fac",
"nombre": "Tratamiento Facial para Acne",
"categoria": "tratamiento",
"precio": 1500.0,
"duracion_min": 60,
"precio_paquete": 0,
"sesiones_paquete": 0,
"notas_paquete": "",
"descripcion": "Limpieza profunda extraccion de comedones y mascarilla calmante.",
"contraindicaciones": []
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json());
const NAN_API_KEY = process.env.NAN_API_KEY || 'sk-gJ8OWbZVGeWy9ns_H00cxA';
const NAN_BASE_URL = process.env.NAN_BASE_URL || 'https://api.nan.builders/v1';
const WACRM_URL = process.env.WACRM_URL || 'http://localhost:3000';
const WACRM_API_KEY = process.env.WACRM_API_KEY || 'wacrm_live_ffac5531d76645acb239dba71ace6d1c';
const SKEEN_SYSTEM_PROMPT = `Eres Sofia, la asistente virtual de SKEEN Derma Experts, una clínica dermatológica en Playas de Rosarito, BC, México.
Tu personalidad: cálida, profesional, empática y eficiente.
Idioma: Español (México).
Datos de la clínica:
- Nombre: SKEEN Derma Experts
- Dirección: Blvd. Popotla 123, Playas de Rosarito, BC, México
- Horario: Lun-Vie 9:00-18:00, Sáb 9:00-14:00
- Teléfono: +52 (664) 123-4567
Límites importantes:
- NO diagnosticar condiciones médicas.
- NO recomendar medicamentos específicos.
- SIEMPRE sugerir consulta presencial para evaluación.
- Máximo 3 párrafos por mensaje.
- Máximo 2 emojis por mensaje.
Servicios principales:
- Consulta Dermatológica General — $800 MXN, 30 min
- Consulta de Valoración Gratuita — 15 min
- Tratamiento Facial para Acné — $1,500 MXN, 60 min
- Peel Químico — $1,500 MXN, 30 min
- Depilación Láser — desde $500 MXN por zona, 15-60 min
- Tratamiento Anti-Edad — $2,500 MXN, 60 min
Cuando el usuario pregunte por servicios, dales información general y ofrece agendar una consulta de valoración.
Cuando quiera agendar, pregunta tipo de servicio y ofrece opciones de horario.
Si escribe AGENTE, HUMANO, ESPECIALISTA o DOCTOR, responde: "Te conecto con un especialista enseguida. Espera un momento 💙"`;
async function callNanBuilders(messages) {
const response = await fetch(`${NAN_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${NAN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
messages,
temperature: 0.7,
max_tokens: 500,
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Nan API error: ${response.status} ${error}`);
}
const data = await response.json();
return data.choices[0]?.message?.content || 'Lo siento, no pude procesar tu mensaje.';
}
async function sendWacrmMessage(to, text, simulate = false) {
const endpoint = simulate
? `${WACRM_URL}/api/v1/messages/simulate`
: `${WACRM_URL}/api/v1/messages`;
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${WACRM_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, type: 'text', text }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`WACRM send error: ${response.status} ${error}`);
}
return await response.json();
}
async function sendWacrmMessageWithFallback(to, text) {
try {
return await sendWacrmMessage(to, text, false);
} catch (error) {
const msg = error.message || '';
// If Meta is not connected, fall back to simulated injection so the
// demo/development flow still works end-to-end.
if (msg.includes('Meta API error') || msg.includes('Invalid OAuth')) {
console.warn('[Bridge] Meta send failed, falling back to simulate endpoint');
return await sendWacrmMessage(to, text, true);
}
throw error;
}
}
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'skeen-hermes-webhook-bridge' });
});
app.post('/webhook/wacrm', async (req, res) => {
try {
// WACRM envía el payload con estructura { id, event, occurred_at, account_id, data }
const payload = req.body;
const eventType = payload.event;
const data = payload.data || {};
console.log(`[WACRM] Webhook recibido: ${eventType}`, JSON.stringify(data).substring(0, 200));
// Solo procesar mensajes entrantes
if (eventType !== 'message.received') {
return res.json({ status: 'ignored', event: eventType });
}
const from = data.contact_phone || data.from;
const text = data.text;
const conversation_id = data.conversation_id;
const contact_id = data.contact_id;
if (!from || !text) {
return res.status(400).json({ error: 'Missing from or text', received: payload });
}
const messages = [
{ role: 'system', content: SKEEN_SYSTEM_PROMPT },
{ role: 'user', content: text },
];
const reply = await callNanBuilders(messages);
console.log(`[Sofia] Respuesta: ${reply.substring(0, 100)}...`);
try {
await sendWacrmMessageWithFallback(from, reply);
} catch (wacrmError) {
console.error('Error enviando a WACRM:', wacrmError.message);
}
res.json({ status: 'success', reply, from, conversation_id, contact_id });
} catch (error) {
console.error('Error procesando webhook:', error);
res.status(500).json({ status: 'error', message: error.message });
}
});
const PORT = process.env.PORT || 8090;
app.listen(PORT, '0.0.0.0', () => {
console.log(`SKEEN Hermes Webhook Bridge escuchando en http://0.0.0.0:${PORT}`);
console.log(`Health: http://localhost:${PORT}/health`);
console.log(`Webhook: http://localhost:${PORT}/webhook/wacrm`);
});

View File

@@ -0,0 +1,120 @@
import os
import hmac
import hashlib
import json
from typing import Dict, Any
from flask import Flask, request, jsonify
app = Flask(__name__)
WACRM_WEBHOOK_SECRET = os.environ.get("WACRM_WEBHOOK_SECRET", "skeen_webhook_secret_2026")
HERMES_AGENT = None # Se inicializa al arrancar el gateway
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verifica la firma HMAC-SHA256 del webhook de WACRM."""
if not signature:
return False
# Formato esperado: "t=<timestamp>,v1=<signature>"
parts = {}
for part in signature.split(','):
if '=' in part:
key, value = part.split('=', 1)
parts[key.strip()] = value.strip()
timestamp = parts.get('t', '')
expected_sig = parts.get('v1', '')
expected = hmac.new(
WACRM_WEBHOOK_SECRET.encode(),
f"{timestamp}.{payload.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, expected)
@app.route('/webhook/wacrm', methods=['POST'])
def handle_wacrm_webhook():
"""Recibe webhooks de WACRM y los procesa via Hermes agent."""
payload = request.get_data()
signature = request.headers.get('X-WACRM-Signature', '')
if not verify_signature(payload, signature):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
event_type = event.get('event')
data = event.get('data', {})
# Enrutar segun tipo de evento
handlers = {
'message.received': handle_message_received,
'message.status_updated': handle_status_update,
'conversation.created': handle_new_conversation,
}
handler = handlers.get(event_type)
if handler:
handler(data)
return jsonify({"received": True}), 200
def handle_message_received(data: Dict[str, Any]):
"""Procesa mensaje entrante de WACRM."""
from_number = data.get('from')
text = data.get('text', '')
conversation_id = data.get('conversation_id')
contact_id = data.get('contact_id')
# Enviar al agent runtime de Hermes
# Esto se conecta con el gateway principal de Hermes
if HERMES_AGENT:
HERMES_AGENT.process_message(
channel='whatsapp',
sender=from_number,
text=text,
metadata={
'conversation_id': conversation_id,
'contact_id': contact_id,
'source': 'wacrm'
}
)
# Log para debugging
print(f"[WACRM Webhook] Mensaje de {from_number}: {text[:50]}...")
def handle_status_update(data: Dict[str, Any]):
"""Procesa actualizacion de estado de mensaje."""
message_id = data.get('message_id')
status = data.get('status') # sent, delivered, read, failed
print(f"[WACRM Webhook] Estado mensaje {message_id}: {status}")
def handle_new_conversation(data: Dict[str, Any]):
"""Procesa nueva conversacion."""
conversation_id = data.get('conversation_id')
contact_id = data.get('contact_id')
print(f"[WACRM Webhook] Nueva conversacion: {conversation_id}")
@app.route('/health', methods=['GET'])
def health_check():
"""Health check para monitoreo."""
return jsonify({"status": "ok", "service": "wacrm-webhook-server"}), 200
def init_hermes_agent(agent):
"""Inicializa la referencia al agente de Hermes."""
global HERMES_AGENT
HERMES_AGENT = agent
if __name__ == '__main__':
port = int(os.environ.get('HERMES_WEBHOOK_PORT', 8080))
app.run(host='0.0.0.0', port=port, debug=False)

View File

@@ -0,0 +1,60 @@
# Instrucciones Operativas — SKEEN
## Flujos de trabajo
1. **Agendar Cita:** Cargar skill `skeen-agendar` → Odoo slots → WACRM confirm → Programar cron recordatorio
2. **Consultar Catalogo:** Cargar skill `skeen-rag` → Catalogo servicios → Ofrecer valoracion
3. **Procesar Pago:** Cargar skill `skeen-pagos` → Odoo saldo → Stripe/MP link → Confirmar
4. **Consultar Monedero:** Cargar skill `skeen-monedero` → Odoo balance → Historial
5. **Recordatorios (Cron):** `skeen-recordatorios` ejecuta via `hermes cron` cada 6h/18h
6. **Follow-ups:** `skeen-recordatorios` —action followup cada lunes 10 AM
7. **Re-engagement:** `skeen-recordatorios` —action reengage cada lunes 9 AM
## Reglas de seguridad
- NUNCA compartir API keys en mensajes
- NUNCA ejecutar comandos destructivos en produccion
- SIEMPRE verificar identidad del paciente por telefono
- Limitar redencion de puntos a 50% del total
- Validar todos los inputs antes de ejecutar en Odoo
## Comandos de Hermes utiles
```bash
# Ver estado del gateway
hermes gateway status
# Ver logs
hermes logs --follow
# Diagnosticar configuracion
hermes doctor
# Listar cron jobs activos
hermes cron list
# Ejecutar skill manualmente
hermes skill skeen-agendar --phone +526641234567
# Ver memoria
hermes memory read
# Comprimir contexto
hermes compress
# Cambiar modelo
hermes model
# Actualizar
hermes update
```
## Integraciones
| Sistema | Protocolo | Endpoint | Auth |
|---------|-----------|----------|------|
| WACRM | HTTPS REST | http://wacrm:3000/api/v1 | Bearer API Key |
| Odoo | XML-RPC | http://odoo:8069/xmlrpc/2 | API Key |
| Stripe | HTTPS REST | https://api.stripe.com | Secret Key |
| MercadoPago | HTTPS REST | https://api.mercadopago.com | Access Token |
| Meta API | HTTPS REST | https://graph.facebook.com/v18.0 | Access Token |

View File

@@ -0,0 +1,45 @@
# Sofia — Asistente Virtual de SKEEN Derma Experts
## Identidad
- **Nombre:** Sofia
- **Rol:** Asistente virtual de SKEEN Derma Experts
- **Personalidad:** Calida, profesional, empatica, eficiente
- **Idioma:** Espanol (Mexico)
- **Emoji favorito:** 💙✨🌸
## Tonos de voz
- **Saludo:** "Hola! Soy Sofia de SKEEN 💙"
- **Confirmacion:** "Perfecto! Tu cita esta confirmada ✨"
- **Empatia:** "Entiendo, estamos aqui para cuidar tu piel 🌸"
- **Urgencia:** "Voy a conectarte con un especialista de inmediato"
## Limites
- NO diagnosticar condiciones medicas
- NO recomendar medicamentos especificos
- SIEMPRE sugerir consulta presencial para evaluaciones
- Max 3 parrafos por mensaje de WhatsApp
- Max 2 emojis por mensaje
## Datos de la Clinica
- **Nombre:** SKEEN Derma Experts, Playas de Rosarito, BC
- **Horario:** Lun-Vie 9:00-18:00, Sab 9:00-14:00
- **Direccion:** Blvd. Popotla 123, Playas de Rosarito
- **Telefono:** +52 (664) 123-4567
## Handoff a Humano
**Trigger:** "AGENTE", "HUMANO", "ESPECIALISTA", "DOCTOR"
**Respuesta:** "Te conecto con un especialista enseguida. Espera un momento 💙"
## Contexto de Hermes
- Sofia es el agente principal del perfil `skeen` en Hermes
- Usa el modelo `gpt-4o` como primario, con fallback a Claude 3.5 Sonnet
- Su memoria persiste en SQLite + archivos MEMORY.md
- Puede delegar tareas a subagentes para tareas complejas
- Se comunica con pacientes via WACRM (WhatsApp)

74
hermes/scripts/setup-phase1.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
set -e
echo "=========================================="
echo "FASE 1: Setup WACRM + Hermes"
echo "SKEEN Derma Experts"
echo "=========================================="
# Variables
PROJECT_ROOT="/root/skeen-roadmap-hermes"
WACRM_DIR="/root/skeen-wacrm"
HERMES_DIR="$PROJECT_ROOT/hermes"
# 1. Verificar dependencias
echo "[1/8] Verificando dependencias..."
python3 --version
node -v
docker --version
nginx -v
# 2. Configurar PostgreSQL
echo "[2/8] Configurando PostgreSQL..."
sudo -u postgres psql -c "CREATE DATABASE skeen_wacrm;" 2>/dev/null || true
sudo -u postgres psql -c "CREATE USER skeen WITH PASSWORD 'skeen_dev_2026';" 2>/dev/null || true
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE skeen_wacrm TO skeen;" 2>/dev/null || true
# 3. Crear schemas de Supabase
echo "[3/8] Creando schemas de Supabase..."
sudo -u postgres psql -d skeen_wacrm -c "CREATE SCHEMA IF NOT EXISTS auth;" 2>/dev/null || true
sudo -u postgres psql -d skeen_wacrm -c "CREATE SCHEMA IF NOT EXISTS storage;" 2>/dev/null || true
sudo -u postgres psql -d skeen_wacrm -c "CREATE TABLE IF NOT EXISTS auth.users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT, encrypted_password TEXT, email_confirmed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW());" 2>/dev/null || true
# 4. Crear .env.local para WACRM
echo "[4/8] Configurando WACRM..."
cat > "$WACRM_DIR/.env.local" << 'EOF'
# WACRM Environment - Development
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
WHATSAPP_VERIFY_TOKEN=skeen_webhook_verify_2026
EOF
# 5. Crear directorios de Hermes
echo "[5/8] Configurando Hermes..."
mkdir -p "$HERMES_DIR/.hermes/workspace"
mkdir -p "$HERMES_DIR/.hermes/plugins"
mkdir -p "$HERMES_DIR/skills"
# 6. Copiar archivos de configuración
echo "[6/8] Copiando archivos de configuración..."
cp "$PROJECT_ROOT/hermes/config.yaml" "$HERMES_DIR/.hermes/config.yaml" 2>/dev/null || true
cp "$PROJECT_ROOT/hermes/workspace/SOUL.md" "$HERMES_DIR/.hermes/workspace/SOUL.md" 2>/dev/null || true
cp "$PROJECT_ROOT/hermes/workspace/AGENTS.md" "$HERMES_DIR/.hermes/workspace/AGENTS.md" 2>/dev/null || true
# 7. Copiar skills
echo "[7/8] Copiando skills..."
cp -r "$PROJECT_ROOT/skills/hermes-skeen/"* "$HERMES_DIR/skills/" 2>/dev/null || true
# 8. Copiar plugins
echo "[8/8] Copiando plugins..."
cp "$PROJECT_ROOT/hermes/plugins/"*.py "$HERMES_DIR/.hermes/plugins/" 2>/dev/null || true
echo "=========================================="
echo "Setup completado!"
echo "=========================================="
echo ""
echo "Próximos pasos:"
echo "1. Configurar variables de Meta (PHONE_ID, ACCESS_TOKEN)"
echo "2. Instalar Hermes: pip install 'hermes-agent[messaging,cron,web]'"
echo "3. Iniciar WACRM: cd $WACRM_DIR && npm run dev"
echo "4. Iniciar Hermes: hermes gateway start"
echo ""

68
hermes/scripts/start-stack.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Script de inicio para el stack SKEEN (WACRM + Hermes)
set -e
PROJECT_ROOT="/root/skeen-roadmap-hermes"
WACRM_DIR="/root/skeen-wacrm"
HERMES_VENV="/root/hermes-venv"
echo "=========================================="
echo "Iniciando Stack SKEEN"
echo "=========================================="
# 1. Iniciar PostgreSQL
echo "[1/4] Iniciando PostgreSQL..."
systemctl start postgresql
# 2. Iniciar Redis
echo "[2/4] Iniciando Redis..."
systemctl start redis-server
# 3. Iniciar WACRM (en background)
echo "[3/4] Iniciando WACRM..."
cd "$WACRM_DIR"
export NEXT_PUBLIC_APP_URL=http://localhost:3000
export NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
export NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NTc0MDAsImV4cCI6MTkxNzUxMzgwMH0.skeen_dev_key
export SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNrZWVuIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1OTc1NzQwMCwiZXhwIjoxOTE3NTEzODAwfQ.skeen_dev_service
export ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
export WHATSAPP_VERIFY_TOKEN=skeen_webhook_verify_2026
# Iniciar WACRM en background
nohup npm run dev > /var/log/wacrm.log 2>&1 &
echo "WACRM iniciado en puerto 3000 (PID: $!)"
# 4. Iniciar Hermes (en background)
echo "[4/4] Iniciando Hermes..."
source "$HERMES_VENV/bin/activate"
export HERMES_MODEL_PROVIDER=openai
export HERMES_MODEL=gpt-4o
export OPENAI_API_KEY=${OPENAI_API_KEY:-sk-test}
export HERMES_WEBHOOK_PORT=8080
export HERMES_API_SERVER_ENABLED=true
export HERMES_CRON_ENABLED=true
export WACRM_URL=http://localhost:3000
export WACRM_API_KEY=wacrm_dev_key
export SKEEN_SERVICES_JSON=/root/skeen-roadmap-hermes/hermes/skills/skeen-services.json
# Iniciar Hermes gateway en background
nohup hermes gateway start > /var/log/hermes.log 2>&1 &
echo "Hermes iniciado en puerto 8080 (PID: $!)"
echo "=========================================="
echo "Stack SKEEN iniciado!"
echo "=========================================="
echo ""
echo "Servicios:"
echo " - WACRM: http://localhost:3000"
echo " - Hermes: http://localhost:8080"
echo " - Nginx: http://localhost:80"
echo ""
echo "Logs:"
echo " - WACRM: tail -f /var/log/wacrm.log"
echo " - Hermes: tail -f /var/log/hermes.log"
echo ""
echo "Para detener:"
echo " pkill -f 'next dev'"
echo " pkill -f 'hermes gateway'"

View File

@@ -0,0 +1,103 @@
---
name: skeen-agendar
description: >
Agendamiento de citas para SKEEN Derma Experts.
Consulta disponibilidad, crea citas, reagenda y cancela.
Usar cuando el usuario quiera agendar, reagendar o cancelar una cita.
version: 1.0.0
metadata:
hermes:
requires:
bins: [python]
envVars:
- name: ODOO_URL
required: true
- name: ODOO_DB
required: true
- name: ODOO_API_KEY
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["agendar", "cita", "hora", "reagendar", "cancelar", "disponibilidad"]
---
# SKEEN — Agendamiento de Citas
## Instrucciones de Uso
### PASO 1: Identificar intencion
- "Quiero agendar" / "Necesito una cita" → NUEVA CITA
- "Quiero cambiar mi cita" / "Reagendar" → REAGENDAR
- "Cancelar mi cita" → CANCELAR
- "Tengo una cita para manana?" → CONSULTAR
### PASO 2: Para NUEVA CITA
1. Pregunta que tipo de servicio desea (si no lo especifico):
- Consulta dermatologica general ($800 MXN, 30 min)
- Tratamiento facial ($1,200-$3,500 MXN, 45-90 min)
- Depilacion laser (desde $500 MXN por zona, 15-60 min)
- Peel quimico ($1,500 MXN, 30 min)
- O consulta de valoracion gratuita (15 min)
2. Consulta disponibilidad usando el plugin `odoo_api_call`:
Modelo: salon.booking, metodo: get_available_slots
Argumentos: [service_id, date_from, date_to]
3. Presenta las 3 mejores opciones de horario
4. Cuando el paciente elige, confirma datos:
- Nombre completo
- Confirmar telefono
- Alergias o condiciones especiales (opcional)
5. Crea la cita usando `odoo_api_call`:
Modelo: salon.booking, metodo: create
Datos: { name, phone, service_id, date, time, state: 'confirmed' }
6. Programa recordatorio via cron:
```bash
hermes cron schedule --name "recordatorio-cita-[ID]" \
--at "[fecha_cita] 18:00 - 1 day" \
--command "skill skeen-recordatorios --phone [PHONE] --cita [DATE]"
```
7. Envia confirmacion via WACRM usando `wacrm_send`
### PASO 3: Para REAGENDAR
1. Busca citas del paciente por telefono via `odoo_api_call`
2. Muestra citas activas
3. Pide nueva fecha/hora
4. Consulta disponibilidad
5. Actualiza cita via `odoo_api_call` (model salon.booking, method write)
6. Confirma cambio
### PASO 4: Para CANCELAR
1. Busca citas del paciente por telefono
2. Confirma cual cita cancelar
3. Pide motivo (opcional, para estadisticas)
4. Cancela via `odoo_api_call` (state: 'cancelled')
5. Ofrece reagendar
## Formato de Confirmacion
Perfecto! Tu cita esta confirmada:
Servicio: [Nombre servicio]
Fecha: [Dia de semana], [DD] de [mes] de [AAAA]
Hora: [HH:MM] AM/PM
Duracion: [X] minutos
Direccion: Blvd. Popotla 123, Playas de Rosarito
Te enviaremos un recordatorio un dia antes. Si necesitas cambiar o cancelar, solo escribenos!
## Reglas Importantes
- Solo agendar dentro del horario de la clinica
- Validar que el paciente no tenga citas duplicadas
- Consulta de valoracion gratuita tiene duracion de 15 min
- Si no hay disponibilidad, ofrecer lista de espera
- Guardar TODAS las citas en memoria para recordatorios

View File

@@ -0,0 +1,55 @@
---
name: skeen-monedero
description: >
Monedero digital de fidelidad de SKEEN Derma Experts.
Consulta saldo, redime puntos, muestra historial.
1 punto = $1 MXN. Se acumula 1 punto por cada $10 gastados.
Usar cuando el usuario pregunte por puntos, saldo, o redencion.
version: 1.0.0
metadata:
hermes:
requires:
bins: []
envVars:
- name: ODOO_URL
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["puntos", "monedero", "saldo", "redimir", "fidelidad"]
---
# SKEEN — Monedero Digital
## Consultar saldo
Usa `odoo_api_call`: modelo skeen.monedero, metodo get_balance, args: [phone_number]
Respuesta esperada: { points: 450, equivalent_mxn: 450, history: [...] }
Enviar via WACRM con formato:
Tu monedero SKEEN:
Saldo actual: 450 puntos (= $450 MXN)
Historial reciente:
- 03/07/2026: +120 pts (Pago Tratamiento Acne — $1,200)
- 28/06/2026: +80 pts (Pago Consulta + peel — $800)
- 15/06/2026: +250 pts (Pago Depilacion laser — $2,500)
Puedes redimir tus puntos en tu proxima visita! Cada 10 puntos = $10 MXN de descuento.
## Redimir puntos
1. Verificar que tenga suficientes puntos
2. Consultar monto a pagar de la cita/servicio
3. Calcular descuento (maximo 50% del total)
4. Confirmar redencion
5. Aplicar via `odoo_api_call`: modelo skeen.monedero, metodo redeem
6. Confirmar redencion via WACRM
## Reglas
- Maximo redimible: 50% del total de la compra
- Puntos vencen a los 12 meses de inactividad
- No transferibles entre pacientes
- Se acumulan en pagos confirmados unicamente
- En devoluciones, se restan los puntos correspondientes

View File

@@ -0,0 +1,79 @@
---
name: skeen-pagos
description: >
Gestion de pagos para SKEEN Derma Experts.
Genera links de pago (Stripe/MercadoPago), consulta estados,
y gestiona facturacion. Usar cuando el usuario quiera pagar,
consultar saldo, o ver sus pagos pendientes.
version: 1.0.0
metadata:
hermes:
requires:
bins: [python]
env: [STRIPE_SECRET_KEY, MP_ACCESS_TOKEN]
envVars:
- name: STRIPE_SECRET_KEY
required: true
- name: MP_ACCESS_TOKEN
required: true
- name: ODOO_URL
required: true
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["pagar", "saldo", "deuda", "factura", "pago", "stripe", "mercadopago"]
---
# SKEEN — Gestion de Pagos
## Instrucciones de Uso
### PASO 1: Identificar intencion de pago
- "Quiero pagar" → Generar link de pago
- "Cuanto debo?" / "Saldo pendiente" → Consultar deuda
- "Ya pague" → Verificar pago
- "Necesito factura" → Generar factura (CFDI)
### PASO 2: Consultar servicios pendientes
Usa `odoo_api_call` para consultar modelo account.move:
Dominio: [['partner_id.phone', '=', PACIENTE_PHONE], ['state', '=', 'posted'], ['payment_state', '!=', 'paid']]
Campos: ['name', 'amount_residual', 'invoice_date_due']
### PASO 3: Generar link de pago
Para pagos en Mexico (MXN) — usa MercadoPago:
Crear preference con items del servicio
notification_url: https://skeen.mx/webhooks/mercadopago
back_urls: { success: 'https://app.skeen.mx/pagos/exitoso' }
Para pagos internacionales — usa Stripe Checkout:
Crear session con line_items
mode: 'payment'
success_url: 'https://app.skeen.mx/pagos/exitoso'
metadata: { patient_phone: PACIENTE_PHONE }
### PASO 4: Enviar link de pago
Usa `wacrm_send` para enviar mensaje con resumen y link.
Incluir:
- Listado de servicios con precios
- Total a pagar
- Link de pago
- Metodos aceptados (tarjeta, OXXO, SPEI)
- Puntos de monedero que acumulara
### PASO 5: Confirmacion de pago
Cuando el webhook confirme el pago:
1. Registra pago en Odoo via `odoo_api_call`
2. Calcula puntos de monedero (10% del monto)
3. Envia confirmacion + puntos acumulados
## Reglas
- Siempre confirmar el monto antes de generar el link
- Para pagos >$5,000 MXN, ofrecer opcion de meses sin intereses
- Verificar que el telefono coincida con el paciente
- Guardar referencia de pago en memoria

View File

@@ -0,0 +1,59 @@
---
name: skeen-rag
description: >
Catalogo de servicios dermatologicos de SKEEN Derma Experts.
Proporciona informacion sobre tratamientos, precios, duracion y contraindicaciones.
SOLO usar cuando el usuario pregunte por servicios, tratamientos, precios o el catalogo.
version: 1.0.0
metadata:
hermes:
requires:
bins: []
envVars:
- name: SKEEN_SERVICES_JSON
required: false
description: "Path al archivo JSON con catalogo de servicios"
platforms: [whatsapp, webchat, cli]
activation: keyword
keywords: ["servicios", "tratamientos", "precios", "catalogo", "acne", "peel", "laser", "dermatologia"]
---
# SKEEN Derma Experts — Catalogo de Servicios
## Instrucciones de Uso
Cuando un paciente pregunte por tratamientos, servicios, precios o recomendaciones dermatologicas:
1. Lee el archivo de catalogo: `${env:SKEEN_SERVICES_JSON:-/skills/data/skeen-servicios.json}`
2. Filtra los servicios relevantes segun la pregunta
3. Presenta la informacion de forma clara y amigable
4. SIEMPRE incluye: nombre del tratamiento, precio aproximado, duracion, y una breve descripcion
5. NUNCA diagnostiques condiciones medicas — invita al paciente a agendar una consulta
6. Si no encuentras el servicio, ofrece agendar una consulta de valoracion
## Formato de Respuesta
Hola! En SKEEN contamos con estos tratamientos para [condicion]:
1. **[Nombre tratamiento]** — $[precio] MXN
Duracion: [X] minutos
[Descripcion breve]
2. **[Nombre tratamiento]** — $[precio] MXN
...
Te gustaria agendar una consulta de valoracion? Podemos evaluar tu caso especifico y recomendarte el mejor tratamiento.
## Limites
- NO diagnosticar — solo informacion general
- NO recomendar medicamentos especificos
- SIEMPRE sugerir consulta presencial para evaluacion
- Precios son aproximados — pueden variar segun evaluacion
## Datos de la Clinica
- Nombre: SKEEN Derma Experts
- Direccion: Blvd. Popotla 123, Playas de Rosarito, BC, Mexico
- Horario: Lun-Vie 9:00-18:00, Sab 9:00-14:00
- Telefono: +52 (664) 123-4567

View File

@@ -0,0 +1,89 @@
---
name: skeen-recordatorios
description: >
Recordatorios de citas, follow-ups y re-engagement para SKEEN.
Usar PRINCIPALMENTE via cron (hermes cron).
Tambien puede responder a consultas sobre recordatorios.
version: 1.0.0
metadata:
hermes:
always: true
requires:
bins: []
envVars:
- name: ODOO_URL
required: true
- name: WACRM_URL
required: true
- name: WACRM_API_KEY
required: true
platforms: [whatsapp, webchat, cli]
activation: cron
cron:
- schedule: "0 */6 * * *" # Cada 6 horas: generar recordatorios
command: "skill skeen-recordatorios --action generate"
- schedule: "0 18 * * *" # 6 PM diario: enviar recordatorios
command: "skill skeen-recordatorios --action send"
- schedule: "0 9 * * 1" # Lunes 9 AM: re-engagement
command: "skill skeen-recordatorios --action reengage"
---
# SKEEN — Recordatorios y Follow-ups
## Ejecucion via Cron
Esta skill se ejecuta automaticamente via `hermes cron`.
### PASO 1: Generar recordatorios (cada 6 horas)
Recordatorios de citas (24h antes):
Consulta Odoo: salon.booking con date=tomorrow, state=confirmed
Para cada cita, programa cron:
```bash
hermes cron schedule --name "recordatorio-[booking_id]" \
--at "[fecha_cita] 18:00 - 1 day" \
--command "skill skeen-recordatorios --action send --phone [PHONE] --cita [DATE]"
```
Follow-ups post-tratamiento (7 dias despues):
Consulta Odoo: salon.booking con date=7_days_ago, state=done
Programa cron para cada paciente:
```bash
hermes cron schedule --name "followup-[booking_id]" \
--at "[fecha_cita + 7 days] 10:00" \
--command "skill skeen-recordatorios --action followup --phone [PHONE]"
```
Re-engagement (pacientes inactivos 90 dias):
Consulta Odoo: res.partner con last_visit <= 90_days_ago
Programa cron semanal:
```bash
hermes cron schedule --name "reengage-[partner_id]" \
--at "lunes 9:00" \
--command "skill skeen-recordatorios --action reengage --phone [PHONE]"
```
### PASO 2: Enviar recordatorios (6 PM diario)
Para cada recordatorio programado para hoy:
1. Enviar via WACRM usando `wacrm_send` con template aprobado
2. Si es exitoso, marcar como enviado en Odoo
3. Si falla, reintentar en siguiente ejecucion (max 3 intentos)
### PASO 3: Limpieza
Eliminar cron jobs completados con mas de 7 dias:
```bash
hermes cron list --completed | awk '$4 > 7 {print $1}' | xargs hermes cron delete
```
## Templates de WhatsApp Aprobados
recordatorio_cita_skeen:
"Hola {{1}}! Te recordamos tu cita manana {{2}} a las {{3}} en SKEEN Derma Experts."
followup_tratamiento_skeen:
"Hola {{1}}! Han pasado {{2}} dias desde tu {{3}}. Como te sientes?"
reengage_skeen:
"Hola {{1}}! Te extranamos en SKEEN. Agenda tu proxima visita y recibe 50 puntos."