import: contenido inicial de la skill whatsapp

This commit is contained in:
2026-04-26 14:26:33 -06:00
parent 5939b530f7
commit cd30283cfb
7 changed files with 696 additions and 1 deletions

237
SKILL.md Normal file
View File

@@ -0,0 +1,237 @@
---
name: whatsapp
description: Consulta y envía mensajes de WhatsApp del beneficio Rio Frio a través del servicio whatsapp-bridge desplegado en nucleo001 (https://whatsapp.nucleoriofrio.com). Úsala cuando el usuario pida buscar, resumir, listar, extraer o enviar información a chats o grupos de WhatsApp de ese número. Cubre casos como "qué se habló en el grupo X", "alguien mencionó retenciones esta semana", "cuál fue el último precio discutido", "mandale un mensaje a Y".
allowed-tools: Bash, Read, Grep
---
# WhatsApp Bridge — skill de consulta on-demand
## Qué es este servicio
`whatsapp-bridge` es un servicio de Nucleo que mantiene una sesión de WhatsApp
vía Baileys contra el número **+504 9742-9865** del beneficio Rio Frio. Guarda
todos los mensajes de los chats (individuales y grupos) a los que ese número
pertenece en Postgres, con retención infinita.
La API expone consultas read + write. **El endpoint `/send` está habilitado**.
- **Base URL**: `https://whatsapp.nucleoriofrio.com`
- **Auth**: `Authorization: Bearer $WA_BRIDGE_TOKEN`
- **El token vive en**: `~/.claude/skills/whatsapp/.env` (local, no versionado)
**Nota sobre el endpoint MCP**: `https://whatsapp.nucleoriofrio.com/mcp` existe
para que Claude.ai web/móvil consuma el mismo servicio como Custom Integration.
**No lo uses tú desde Claude Code** — tu path principal es la API REST vía
`scripts/query.sh`. El MCP es solo para los otros clientes de Claude.
## Cómo invocarla correctamente
Antes de cualquier query:
1. Carga el token: `source ~/.claude/skills/whatsapp/.env` (o lee `$WA_BRIDGE_TOKEN` del entorno).
2. Si no hay token, avisa al usuario: pedir que cree `~/.claude/skills/whatsapp/.env` con `WA_BRIDGE_TOKEN=<valor>`.
3. Verifica estado: `GET /api/auth/status`. Si `status !== "open"`, **no hagas queries** — avísale al usuario que el bridge está caído o necesita re-pairing.
El helper `scripts/query.sh` ya maneja el bearer:
```bash
~/.claude/skills/whatsapp/scripts/query.sh /api/auth/status
```
## Endpoints disponibles
Todos bajo `https://whatsapp.nucleoriofrio.com` con `Authorization: Bearer $TOKEN`.
### Diagnóstico
- `GET /healthz` — liveness, sin auth. Úsalo para verificar que el servicio responde.
- `GET /api/auth/status``{ status, hasSession, user, pairingCode, lastError }`.
Valores de `status`: `idle | connecting | open | closed | needs-pair`.
### Listar chats
- `GET /api/chats?type=<group|individual|all>&q=<texto>&limit=100&offset=0`
- `type`: filtrar por tipo. Default `all`.
- `q`: búsqueda parcial por nombre del chat o JID.
- Responde: `{ count, chats: [{ jid, isGroup, name, lastMessageAt, participants }] }`.
- **Patrón típico**: cuando el usuario menciona un grupo por nombre (ej "el grupo de productores"), primero `GET /api/chats?type=group&q=productores` para resolver el JID, después consulta mensajes con ese JID.
### Un grupo específico
- `GET /api/groups/:jid` — metadata (live + stored). El `:jid` debe ser URL-encoded (termina en `@g.us`).
- `GET /api/groups/:jid/messages` — mensajes del grupo. Query params:
- `since=2026-04-14T00:00:00Z` (ISO timestamp)
- `until=2026-04-21T23:59:59Z`
- `q=<texto>` — búsqueda parcial ILIKE en el contenido
- `sender=<jid>` — filtrar por quién escribió (participant)
- `order=asc|desc` (default `desc`)
- `limit=100` (máx 1000), `offset=0`
- Responde: `{ jid, count, filters, messages: [{ msgId, fromMe, senderJid, pushName, messageType, content, contextInfo, timestamp }] }`.
### Chats individuales
- `GET /api/messages/individual/:jid?since=&until=&q=&order=&limit=&offset=`
- `:jid` termina en `@s.whatsapp.net`.
- Responde mensajes de ese chat.
### Búsqueda cross-chat
- `GET /api/messages/search?q=<texto>&since=&until=&chatType=<group|individual|all>&limit=100`
- Busca en TODO el historial. Úsalo cuando el usuario pide algo como "alguien mencionó retenciones" o "busca el precio por quintal de la semana pasada".
- Responde: `{ query, count, matches: [{ chatJid, msgId, senderJid, pushName, isGroup, content, timestamp }] }`.
- Para pasar de matches a contexto, obtén el `chatJid` de un match y pide `GET /api/groups/:jid/messages` con `since` unos minutos antes.
### Endpoints de escritura
- `POST /api/messages/send` — texto.
Body: `{ jid, text, quotedChatJid?, quotedMsgId?, mentions? }`.
`quotedMsgId` opcional para **responder** a un mensaje existente (pass quotedChatJid si es de otro chat).
`mentions` array de JIDs para @-menciones.
- `POST /api/messages/forward` — reenviar un mensaje del historial a otro chat.
Body: `{ sourceChatJid, sourceMsgId, destJid }`.
El mensaje debe existir en nuestro storage (lo vio el socket en vivo).
- `POST /api/messages/react` — reaccionar con emoji.
Body: `{ chatJid, msgId, emoji }`. Emoji vacío `""` = quitar reacción previa.
- `POST /api/messages/send-media` — multimedia (image/video/audio/document).
Body: `{ jid, url, type, caption?, fileName?, mimetype?, ptt?, quotedChatJid?, quotedMsgId? }`.
La URL debe ser HTTP(S) público — IPs privadas bloqueadas por SSRF guard. `type` es obligatorio.
- `POST /api/messages/send-location` — ubicación geográfica.
Body: `{ jid, latitude, longitude, name?, address?, quotedChatJid?, quotedMsgId? }`.
- `POST /api/messages/send-poll` — encuesta.
Body: `{ jid, name, values[2..12], selectableCount?, quotedChatJid?, quotedMsgId? }`.
`selectableCount=1` (default) = single-choice; `=0` = multi-choice; `=N` = hasta N opciones.
- `POST /api/messages/send-contact` — tarjeta(s) de contacto vCard. Hasta 20.
Body: `{ jid, contacts: [{ displayName, firstName?, lastName?, phone?, phoneDisplay?, email?, organization?, title?, vcard? }], quotedChatJid?, quotedMsgId? }`.
Si `phone` está en E.164 sin `+`, WhatsApp lo detecta como contacto vinculable (tap → chatear).
**Eventos**: NO soportado todavía. Baileys 6.17 no tiene API estable para crear event messages. Si el user lo pide, avisale del estado actual.
### Descarga de archivos (image/video/audio/document/sticker)
Los mensajes en respuestas de `/groups/:jid/messages`, `/individual/:jid`, `/search` incluyen un campo `media` cuando contienen archivo:
```json
"media": {
"type": "image",
"mimetype": "image/jpeg",
"size": 123456,
"caption": "...",
"downloadUrl": "/api/messages/media/<encoded-chatJid>/<msgId>"
}
```
Para obtener el archivo **desencriptado**:
```bash
~/.claude/skills/whatsapp/scripts/query.sh \
"/api/messages/media/<encoded-chatJid>/<msgId>" \
-o /tmp/wa-image.jpg
```
Eso guarda el binario. `query.sh` pasa el bearer automáticamente; la ruta que copies del campo `downloadUrl` ya funciona.
Para solo metadata sin descargar: `GET /api/messages/media-info/:chatJid/:msgId`.
Todos retornan `{ ok: true, msgId: "..." }` al éxito, 4xx/5xx con `{ error: "..." }` si falla.
Si retornan 403 significa que `ENABLE_SEND=false` — hay que cambiarlo en `stack.yml` y redeployar.
### Pairing (setup / recovery)
- `POST /api/auth/pair` → devuelve `{ code }` (válido ~60s).
- El usuario toma su teléfono con el número, abre WhatsApp > Ajustes > Dispositivos vinculados > Vincular con número, y teclea el código.
- `POST /api/auth/logout` con header `X-Confirm-Wipe: yes` → borra la sesión. **DESTRUCTIVO**: después hay que re-pairear. Nunca llamar esto salvo que el usuario lo pida explícitamente.
## Reglas de comportamiento
### Antes de ejecutar queries
- Si el status no es `open`, avísale al usuario y no consumas la API en vano.
- Si el usuario menciona un grupo por nombre, resuelve el JID primero con `/api/chats?type=group&q=…` y pídele confirmar "¿te refieres a este grupo: <subject>?".
### Cuándo usar qué endpoint
- **Resumir un grupo en una ventana de tiempo** → `/api/groups/:jid/messages?since=&until=` con `order=asc` para leer en orden cronológico.
- **Buscar menciones** (palabra clave) → `/api/messages/search?q=` primero; si hay resultados, fetch contexto con `/api/groups/:jid/messages?since=&until=` alrededor del match.
- **Ver actividad reciente** → `/api/chats?type=group&limit=20` ordena por `lastMessageAt` desc.
- **Perfilar un contacto** (cuánto escribió, en qué chats) → `/api/messages/search?q=&sender=…` (no implementado aún — si es necesario, usa el endpoint de grupo con `sender=` repetidamente).
### Cómo mostrar resultados al usuario
- Los JIDs (`549…@g.us`) son ruido visual. Muestra `pushName` o `name` del chat, no el JID, salvo que el usuario lo pida explícitamente.
- Agrupa por chat cuando haya resultados de múltiples chats.
- Si la lista es larga (>30 mensajes), resume en lugar de volcar todo.
- Incluye el `timestamp` en formato humano (ej `2026-04-21 15:30`) y `pushName` del autor.
- Mensajes con `fromMe: true` son del número del bridge (no los confundas con respuestas de otros).
### Errores típicos y cómo reaccionar
- **401 unauthorized** → el token está mal. Revisa `WA_BRIDGE_TOKEN` en `~/.claude/skills/whatsapp/.env`.
- **503 socket not ready** → el bridge está caído o desconectado. `GET /api/auth/status` para diagnosticar. Si `status=needs-pair`, el usuario tiene que re-pairear.
- **404 en un JID** → el chat no existe en nuestro storage. El número puede no estar en ese grupo, o el JID está mal escrito.
## Ejemplos de sesiones
### Caso: "Resume lo que se habló hoy en el grupo de operaciones"
```bash
# 1. Resolver el JID
~/.claude/skills/whatsapp/scripts/query.sh '/api/chats?type=group&q=operaciones'
# → { chats: [{ jid: "123@g.us", name: "Operaciones Rio Frio", ... }] }
# 2. Pedir mensajes de hoy
TODAY=$(date -u +%Y-%m-%dT00:00:00Z)
~/.claude/skills/whatsapp/scripts/query.sh "/api/groups/123%40g.us/messages?since=$TODAY&order=asc&limit=500"
# → resumir manualmente en la respuesta al usuario
```
### Caso: "¿Alguien mencionó el precio por quintal esta semana?"
```bash
WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%dT00:00:00Z)
~/.claude/skills/whatsapp/scripts/query.sh "/api/messages/search?q=quintal&since=$WEEK_AGO&chatType=group"
# → presentar los matches agrupados por chat con timestamp y autor
```
### Caso: "Envía un mensaje al grupo X diciendo Y"
POST:
```bash
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
-H 'Content-Type: application/json' \
-d '{"jid":"123@g.us","text":"..."}' /api/messages/send
```
Si responde 403, avisale al user que `ENABLE_SEND=false` en `stack.yml`.
### Caso: "Responde a ese mensaje con X"
Pasá `quotedMsgId` (y opcional `quotedChatJid` si es de otro chat):
```bash
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
-H 'Content-Type: application/json' \
-d '{"jid":"123@g.us","text":"claro, ya vamos","quotedMsgId":"ABCD..."}' \
/api/messages/send
```
### Caso: "Reenvía ese mensaje al grupo de productores"
1. Obtené el JID del grupo destino (vía `/api/chats`).
2. POST a `/api/messages/forward` con `{ sourceChatJid, sourceMsgId, destJid }`.
### Caso: "Reacciona con 👍 a ese mensaje"
```bash
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
-H 'Content-Type: application/json' \
-d '{"chatJid":"123@g.us","msgId":"ABCD","emoji":"👍"}' \
/api/messages/react
```
### Caso: "Mandá esa imagen con el caption X"
El usuario te pasa una URL pública (imgur, Gitea attachment, etc.):
```bash
~/.claude/skills/whatsapp/scripts/query.sh -X POST \
-H 'Content-Type: application/json' \
-d '{"jid":"123@g.us","url":"https://i.imgur.com/X.jpg","type":"image","caption":"..."}' \
/api/messages/send-media
```
Si no tiene URL pública y el archivo está local, primero hay que subirlo a algún lado accesible. No tenemos (aún) endpoint de upload directo multipart.
## Archivos de referencia en esta skill
- `endpoints.md` — tabla de referencia rápida de todos los endpoints y sus parámetros.
- `contacts.md`**LEELO** siempre que vayas a mostrar o enviar a un JID; contiene el mapping de identificadores a personas conocidas.
- `scripts/query.sh` — helper de curl autenticado.
- `.env` (no versionado) — contiene `WA_BRIDGE_TOKEN=<valor>`.