chore: initialize browser tool devlog from project root

This commit is contained in:
Paco 2026-04-24 23:59:41 +02:00
commit 420c6c85bb
28 changed files with 7891 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules/
dist/
artifacts/
playwright-report/
test-results/
.opencode/

101
README.md Normal file
View file

@ -0,0 +1,101 @@
# OpenCode Browser Tool
Herramienta browser externa para OpenCode, pensada para navegar e interactuar con aplicaciones web desde un navegador real controlado por `Playwright + Chromium`, con integracion por `MCP`.
## Estado actual
Proyecto en preparacion de la v1.
La prioridad actual es lograr una integracion efectiva entre OpenCode y el browser manteniendo la solucion totalmente desacoplada del core de OpenCode.
## Onboarding recomendado para agentes nuevos
Al iniciar una sesion en un equipo nuevo, pide primero:
- `browser_help` para descubrir capacidades, defaults y ejemplos
- `browser_health` para ver estado operativo, actividad y artifacts recientes
Default relevante en v1:
- `browser_report` usa `saveToFile=true` por defecto (configurable de forma persistente con `browser_config`)
## Objetivo de la v1
- exponer una tool browser externa por `MCP`
- abrir `Chromium` en modo visible por defecto
- permitir navegacion e interaccion base con apps locales
- recoger evidencia minima util para diagnostico
## Stack fijado para la v1
- `Node.js 20+`
- `TypeScript`
- `Playwright`
- `Chromium`
- `MCP` por `stdio`
## Modos de navegador en runtime
- `testing`: navegador gestionado por Playwright (default)
- `system`: navegador del sistema (ej. `/usr/bin/google-chrome`)
Ambos modos pueden abrir con perfil efimero o perfil persistente (`userDataDir`) segun configuracion y parametros de `browser_open`.
Defaults operativos relevantes:
- `recordVideo=false` (grabacion bajo demanda)
- `verbose=true` y `verboseOverlay=true` en modo visible
- delay humano de interaccion `1000-3000ms` para acciones click-like
## Estructura actual
```text
opencode-browser-tool/
artifacts/
config/
docs/
scripts/
src/
browser/
tools/
types/
check.sh
install.sh
opencode.mcp.example.json
package.json
README.md
tsconfig.json
```
## Documentacion principal
- `docs/PLAN_DE_DESARROLLO.md`
- `docs/TODO.md`
- `docs/manual_de_uso_heramienta.md`
- `docs/QUICKSTART.md`
- `docs/VALIDACION_ENTORNO_LIMPIO.md`
- `docs/PLAN_CIERRE_INSTALABLE.md`
- `docs/REGISTRO_SITUACIONES.md`
- `docs/contexto_workspace/` (snapshot de docs globales clave para continuidad)
## Instalacion prevista
La idea es que este proyecto pueda copiarse o descargarse en un PC con OpenCode y dejarse listo mediante:
- `./install.sh`
- una guia de instalacion clara
- o instrucciones que un agente de OpenCode pueda ejecutar
### Que hace `install.sh` con Chromium
El script instala dependencias y luego ejecuta:
- `npx playwright install chromium`
Esto instala el `Chromium managed by Playwright` para garantizar compatibilidad estable en la v1.
Si el sistema ya tiene un Chromium propio, en esta fase igualmente se usa el gestionado por Playwright.
## Nota
Este proyecto debe seguir siendo externo a OpenCode para que las actualizaciones del propio OpenCode no afecten a esta herramienta.

33
check.sh Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
printf 'Verificando proyecto en %s\n' "$SCRIPT_DIR"
if ! command -v node >/dev/null 2>&1; then
printf 'Node.js no encontrado.\n' >&2
exit 1
fi
if ! command -v npm >/dev/null 2>&1; then
printf 'npm no encontrado.\n' >&2
exit 1
fi
printf 'Node: '
node --version
printf 'npm: '
npm --version
if [ -d "$SCRIPT_DIR/node_modules" ]; then
printf 'Dependencias instaladas: si\n'
else
printf 'Dependencias instaladas: no\n'
fi
if [ -d "$SCRIPT_DIR/artifacts" ]; then
printf 'Carpeta artifacts: lista\n'
else
printf 'Carpeta artifacts: ausente\n'
fi

View file

@ -0,0 +1,16 @@
{
"browser": {
"defaultKind": "testing",
"systemExecutablePath": "/usr/bin/google-chrome",
"defaultPersistentProfile": false,
"defaultUserDataDir": null,
"defaultVerbose": true,
"defaultVerboseOverlay": true,
"defaultInteractionDelayMinMs": 1000,
"defaultInteractionDelayMaxMs": 3000
},
"report": {
"defaultSaveToFile": true,
"defaultFormat": "json"
}
}

View file

@ -0,0 +1,59 @@
# Plan de cierre - Instalable v1
Objetivo: dejar `opencode-browser-tool` listo para puesta en marcha facil en otro PC con OpenCode, sin rutas fijas.
## Orden de ejecucion acordado
1. Ejecutar una prueba funcional final (smoke de aceptacion).
2. Cerrar empaquetado/instalacion portable para Linux.
3. Ejecutar validacion en entorno limpio con checklist.
4. Documentar estado final y criterios de cierre v1.
## Alcance de cierre (Linux primero)
- Instalacion robusta con `install.sh` + verificaciones claras.
- Configuracion MCP sin rutas hardcodeadas.
- Defaults seguros por maquina (`testing` por defecto, perfil persistente opcional).
- Soporte de rutas variables:
- directorio del proyecto
- binario de navegador del sistema
- ruta de perfil persistente
- ubicacion de OpenCode
- Guia de arranque para operario en equipo nuevo.
## Checklist tecnico de empaquetado
- `install.sh`:
- valida `node` y `npm`
- instala dependencias
- compila TypeScript
- instala navegador testing de Playwright
- emite mensaje final de estado
- `check.sh`:
- valida build
- valida carga de config
- valida arranque basico MCP
- Plantilla MCP:
- sin rutas absolutas fijas
- instrucciones para reemplazar path local en cada PC
- Configuracion runtime:
- perfil persistente configurable por ruta local
- navegador `system` opcional por ruta local
## Validacion en entorno limpio
Usar como referencia operativa:
- `docs/VALIDACION_ENTORNO_LIMPIO.md`
Criterio de aceptacion:
- instalacion completa sin error
- MCP conectado en OpenCode
- prueba minima de apertura/navegacion/snapshot
- prueba Google objetivo completable
## Nota sobre Windows (evaluacion pendiente)
La v1 se cierra primero en Linux.
Despues se evaluara soporte Windows con enfoque cross-platform (scripts npm + Node).

368
docs/PLAN_DE_DESARROLLO.md Normal file
View file

@ -0,0 +1,368 @@
# Plan de desarrollo - Browser Tool para OpenCode
## Proyecto
Nombre del proyecto: `opencode-browser-tool`
Objetivo general:
Construir una herramienta browser externa a OpenCode que permita a un agente navegar, inspeccionar e interactuar con aplicaciones web de forma visible por defecto, con soporte posterior para ejecucion headless y capacidades avanzadas de diagnostico.
La herramienta debe quedar desacoplada del nucleo de OpenCode para que futuras actualizaciones de OpenCode no rompan ni obliguen a rehacer la solucion.
---
## Resultado esperado
Al terminar el proyecto, OpenCode debe poder usar la herramienta como una tool externa para:
- abrir un navegador controlado por la herramienta
- entrar en aplicaciones locales o webs externas autorizadas
- interactuar con la interfaz como un usuario real
- inspeccionar DOM, consola y red
- recoger evidencia de ejecucion
- devolver resultados estructurados al agente
---
## Criterios estructurales fijados
- La solucion debe ser externa a OpenCode.
- El browser y su logica no deben vivir dentro del core de OpenCode.
- La integracion con OpenCode debe hacerse por `MCP`.
- La primera version debe priorizar `visible` como modo por defecto.
- `headless` quedara disponible para escenarios tecnicos donde la UI visible no sea el foco principal.
- La base tecnologica inicial sera `Playwright + Chromium`.
- `CDP` se considerara como capacidad complementaria y progresiva, no como dependencia central de la v1.
---
## Arquitectura objetivo por fases
### Fase 1 - Arquitectura simple funcional
Flujo:
`OpenCode -> MCP server externo -> Playwright -> Chromium`
Caracteristicas:
- MCP server externo ejecutado fuera del core de OpenCode
- Playwright como motor principal de automatizacion
- Chromium como navegador base
- gestion simple de sesiones de browser
- artefactos locales: screenshots, logs, traces y video cuando aplique
Ventaja principal:
Permite llegar rapido a una version util sin comprometer la evolucion futura.
### Fase 2 - Arquitectura modular
Flujo previsto:
`OpenCode -> MCP server externo -> backend/orquestador interno -> runner browser`
Caracteristicas previstas:
- separacion entre capa MCP y capa operativa del navegador
- gestion mas fuerte de sesiones, colas, perfiles y artefactos
- posibilidad de varios runners o modos de ejecucion
- mejor punto de extension para seguridad, politicas y control de uso
### Fase 3 - Diagnostico avanzado
Capacidades previstas:
- ganchos `CDP` para inspeccion avanzada de Chromium
- mas telemetria y diagnostico de red, runtime y rendimiento
- capacidades de analisis fino de estados del navegador durante pruebas complejas
---
## Alcance funcional de la v1
La v1 debe resolver correctamente la interaccion efectiva entre OpenCode y el browser.
### Lo que debe incluir
- integracion por `MCP`
- ejecucion visible por defecto
- soporte para abrir navegador e ir a una URL
- acciones basicas: `click`, `type`, `press`, `select`, `scroll`, `hover`
- esperas utiles: selector, texto, URL, carga de pagina
- lectura de informacion de pantalla y DOM
- ejecucion de JavaScript en pagina cuando haga falta
- logica nativa de estabilizacion tras acciones de navegacion/interaccion para reducir esperas manuales
- captura de screenshot
- captura de consola y errores del runtime de la pagina
- soporte para video o tracing si el coste operativo es razonable desde la v1
- respuesta estructurada al agente con estado, error y evidencia
### Lo que puede quedar fuera de la v1 si retrasa demasiado
- backend intermedio separado
- politicas completas para webs externas
- soporte fuerte de perfiles persistentes complejos
- capacidades avanzadas de `CDP`
- compatibilidad cross-browser real con `WebKit` o `Firefox`
---
## Herramientas y componentes previstos
### Base principal
- `Playwright`
- `Chromium`
- `Node.js 20+`
- `TypeScript`
### Integracion
- `MCP server` externo como interfaz con OpenCode
### Diagnostico progresivo
- `CDP` en puntos concretos cuando `Playwright` no cubra suficientemente el caso
### Artefactos
- screenshots
- logs de consola
- resultados estructurados
- video o trace cuando convenga
---
## Contratos que deben quedar bien definidos desde el inicio
Aunque la v1 use arquitectura simple, el diseno debe dejar estables estos contratos:
- contrato de sesion de browser
- contrato de acciones del navegador
- contrato de artefactos generados
- contrato de respuesta al agente
Esto permitira pasar a una arquitectura con backend/orquestador sin rehacer el modelo base.
---
## Stack fijado para la v1
Queda fijado para la v1:
- `Node.js 20+`
- `TypeScript`
- `Playwright`
- `Chromium`
- `@modelcontextprotocol/sdk`
- `MCP` por `stdio`
Decision de navegador para v1:
- usar `Chromium managed by Playwright` como modo por defecto y recomendado
- ejecutar instalacion de Chromium durante `install.sh` mediante `npx playwright install chromium`
- no depender del `Chromium` del sistema en la v1 para evitar variaciones de compatibilidad
Evolucion prevista:
- habilitar en fase posterior un modo opcional `system browser` por `executablePath` para quienes prefieran reutilizar un navegador ya instalado
Queda descartado por ahora:
- usar `Puppeteer` en paralelo a `Playwright`
- introducir `WebKit` en la v1 como motor principal
- construir desde el inicio un backend intermedio obligatorio
---
## Tools MCP minimas de la v1
La v1 se implementara con un conjunto pequeno y suficiente de tools MCP.
Lista inicial fijada:
- `browser_open`
- `browser_close`
- `browser_navigate`
- `browser_click`
- `browser_type`
- `browser_press`
- `browser_scroll`
- `browser_wait`
- `browser_snapshot`
- `browser_evaluate`
Capacidad cubierta por este conjunto:
- apertura y cierre de sesion de browser
- navegacion a URL objetivo
- interaccion base con la interfaz
- esperas para estabilizar ejecuciones
- captura de evidencia visual inicial
- lectura o evaluacion de estado en pagina mediante JavaScript
Capacidades previstas para ampliacion posterior sin romper estos contratos:
- logs de consola como tool separada o adjunta a respuestas
- tracing o video
- lectura estructurada de red
- gestion avanzada de perfiles y sesiones
---
## Estructura inicial fijada del proyecto
La estructura inicial del proyecto queda fijada asi:
```text
opencode-browser-tool/
artifacts/
docs/
PLAN_DE_DESARROLLO.md
TODO.md
scripts/
src/
browser/
tools/
types/
server.ts
.gitignore
check.sh
install.sh
opencode.mcp.example.json
package.json
README.md
tsconfig.json
```
Objetivo de esta estructura:
- separar claramente documentacion, codigo, scripts y artefactos
- dejar lista la base para evolucionar a una arquitectura modular
- mantener el paquete autocontenido para instalarlo en otros equipos con OpenCode
---
## Comunicacion interna prevista
### En la v1
`OpenCode` actuara como cliente `MCP`.
La herramienta expondra un `MCP server` externo que OpenCode podra arrancar y usar.
Ese servidor traducira las tools pedidas por OpenCode a operaciones sobre `Playwright`.
### En la v2
El `MCP server` podra seguir siendo la cara publica, pero delegando ya en un backend intermedio propio.
Esto permite mantener estable la integracion con OpenCode mientras evoluciona el interior del sistema.
---
## Instalacion y distribucion esperadas
La herramienta debe poder distribuirse como carpeta autocontenida del proyecto.
Estructura objetivo aproximada:
```text
opencode-browser-tool/
docs/
src/
scripts/
artifacts/
package.json
README.md
install.sh
check.sh
opencode.mcp.example.json
```
### Objetivo de instalacion
En un PC con OpenCode, debe ser posible dejar la herramienta lista con uno de estos caminos:
- ejecutar un script de instalacion rapida
- seguir instrucciones claras en un `.md`
- pedir a un agente de OpenCode que ejecute esas instrucciones y deje todo listo
### Lo que debe dejar listo la instalacion
- dependencias del proyecto instaladas
- Playwright y Chromium preparados
- scripts de verificacion funcional
- plantilla de configuracion de OpenCode para conectar el `MCP server`
- rutas de artefactos creadas
- instrucciones claras para primer uso
---
## Funcionamiento operativo esperado de la v1
### Configuracion inicial
- el usuario instala el proyecto en una carpeta del sistema
- el usuario ejecuta el script o sigue el `.md` de instalacion
- OpenCode queda configurado para conocer el `MCP server` del browser tool
### Uso normal
- el agente decide usar la herramienta o el usuario se lo ordena
- OpenCode lanza el `MCP server` externo si la integracion se hace por `stdio`
- el `MCP server` llama a `Playwright`
- `Playwright` controla `Chromium`
- la herramienta devuelve al agente resultados y evidencia
### Modos de arranque del MCP server
Se prioriza para la v1:
- `stdio` autolanzado por OpenCode
Queda prevista mas adelante la opcion:
- servicio persistente separado si el crecimiento del sistema lo requiere
---
## V2 prevista
La v2 debe apoyarse en una v1 ya funcional y estable.
Capacidades previstas:
- backend/orquestador intermedio
- gestion mas rica de sesiones y perfiles
- configuracion de seguridad y permisos de uso
- control mas fino de navegacion externa
- artefactos y observabilidad mas avanzados
- puntos de extension para diagnostico con `CDP`
- posible evaluacion futura de `WebKit` para compatibilidad, sin desplazar a Chromium como base inicial
---
## Por definir mas adelante
- politica exacta de uso sobre webs externas
- allowlist de dominios
- confirmacion para acciones sensibles
- gestion de `CAPTCHA` y `2FA`
- necesidad real de soporte `WebKit`
- nivel de persistencia de sesiones entre ejecuciones
- politica final de seleccion entre `managed chromium` y `system browser`
---
## Criterio de exito de la v1
La v1 se considerara lograda cuando:
- OpenCode pueda usar la herramienta browser como tool externa por `MCP`
- el browser se abra en modo visible y responda a acciones reales
- el agente pueda navegar e interactuar con una app local
- la herramienta pueda devolver evidencia suficiente para diagnosticar fallos
- la estructura creada permita evolucionar a v2 sin rehacer la integracion con OpenCode

152
docs/QUICKSTART.md Normal file
View file

@ -0,0 +1,152 @@
# Quickstart - Browser Tool
Guia corta para dejar la herramienta lista y ejecutar una prueba minima.
## 1) Instalar
Desde `opencode-browser-tool/`:
```bash
./install.sh
```
Esto instala dependencias y `Chromium managed by Playwright`.
## 2) Compilar
```bash
npm run build
```
## 3) Configurar MCP en el proyecto
Crear `opencode-browser-tool/.opencode/opencode.json` con:
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"browser-tool": {
"type": "local",
"command": [
"node",
"/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/dist/server.js"
]
}
}
}
```
Nota: ajusta la ruta absoluta de `dist/server.js` a tu equipo.
## 4) Verificar conexion MCP
```bash
opencode mcp list
```
Debe aparecer `browser-tool` como `connected`.
## 5) Primera prueba minima
Pide a OpenCode una secuencia simple:
- abrir navegador
- navegar a una URL
- tomar snapshot
- cerrar navegador
Ejemplo de instruccion:
```text
Usa solo browser-tool: abre navegador visible, navega a https://example.com, toma snapshot con label quickstart y cierra navegador.
```
## 5.1) Auto-descubrimiento recomendado
Antes de pruebas complejas, pide:
- `browser_help`
- `browser_health`
- `browser_diagnostics_clear` (opcional, para empezar limpio)
Con eso el agente detecta capacidades disponibles, defaults y estado operativo actual.
## 6) Dónde quedan las capturas
Se guardan en:
- `opencode-browser-tool/artifacts/`
## 7) Descubrir capacidades automaticamente
Usa las tools `browser_help` y `browser_health` al inicio de una sesion para que el agente reciba capacidades, defaults y estado de ejecucion.
Para incidencias durante pruebas, usa `browser_diagnostics` para extraer consola, errores de pagina y requests fallidas.
Si un flujo no avanza, usa `browser_observe` para capturar estado intermedio y `browser_handle_consent` para resolver banners.
## 8) Grabacion bajo demanda (video/trace)
Para sesiones complejas, abre con grabacion:
- `recordVideo: true`
- `recordTrace: true`
- `recordLabel: "caso-xyz"`
Los artifacts se cierran y quedan disponibles al ejecutar `browser_close`.
## 9) Reporte consolidado de ejecucion
Al finalizar un flujo, puedes pedir:
- `browser_report` con `format: both`
Nota: por defecto la herramienta ya deja `saveToFile=true` al instalar.
Esto deja resumen tecnico en JSON y lectura humana en Markdown dentro de `artifacts/`.
Si quieres cambiar ese default de forma persistente, usa `browser_config` con `action: set`.
## 10) Elegir navegador: testing o system
Por defecto, `browser_open` usa `browserKind: testing` (Playwright managed).
Si necesitas perfil real de usuario, puedes usar navegador del sistema:
```json
{
"browserKind": "system",
"persistentProfile": true,
"userDataDir": "/home/pancho/.chrome-perfil-google-real",
"startUrl": "https://www.google.com"
}
```
Tambien puedes fijarlo de forma persistente con `browser_config` (`browserDefaultKind`, `browserDefaultPersistentProfile`, `browserDefaultUserDataDir`).
## 11) Verbose y delay humano (default activo)
Por defecto, `browser_open` aplica:
- `verbose: true`
- `verboseOverlay: true` (en modo visible)
- `interactionDelayMinMs: 1000`
- `interactionDelayMaxMs: 3000`
Puedes desactivar verbose por ejecucion:
```json
{
"verbose": false,
"verboseOverlay": false
}
```
Y puedes ajustar delays por ejecucion:
```json
{
"interactionDelayMinMs": 800,
"interactionDelayMaxMs": 1800
}
```

View file

@ -0,0 +1,218 @@
# Registro de situaciones detectadas
## Instrucciones para el agente
Cuando el usuario pida documentar un error, incidencia o comportamiento inesperado en pruebas con `browser-tool`, debes:
1. Crear una nueva entrada usando la plantilla de este archivo.
2. Escribir informacion concreta y verificable, sin suposiciones vagas.
3. Incluir siempre evidencia trazable (`artifact`, log, salida de tool o reporte).
4. Indicar estado actual (`pendiente`, `en analisis`, `resuelto`, `descartado`).
5. Si aplicaste una correccion, anotar que se cambio y como se valido.
6. No borrar entradas anteriores; agregar nuevas y, si aplica, actualizar el estado de la entrada ya existente.
7. Usar fecha y hora local en formato `YYYY-MM-DD HH:mm`.
Objetivo: mantener un historial operativo claro para depurar, priorizar y cerrar hallazgos durante desarrollo y pruebas.
---
## Plantilla de entrada
### Entrada (YYYY-MM-DD HH:mm)
- Contexto:
- Flujo probado:
- Situacion observada:
- Resultado esperado:
- Resultado real:
- Evidencia (artifact/log/report):
- Hipotesis de causa:
- Accion aplicada:
- Validacion posterior:
- Estado: pendiente | en analisis | resuelto | descartado
- Notas:
---
## Entradas
### Entrada (2026-04-23 00:00)
- Contexto: Inicializacion del registro.
- Flujo probado: N/A.
- Situacion observada: Se crea el archivo para consolidar hallazgos futuros.
- Resultado esperado: Disponer de un formato unico para registrar incidencias.
- Resultado real: Archivo creado correctamente.
- Evidencia (artifact/log/report): `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md`
- Hipotesis de causa: N/A.
- Accion aplicada: Creacion de plantilla e instrucciones de uso.
- Validacion posterior: Pendiente de primeras entradas reales.
- Estado: resuelto
- Notas: Punto de arranque para seguimiento de situaciones durante pruebas.
### Entrada (2026-04-23 19:42)
- Contexto: Prueba funcional de buscador externo para validar paginacion y localizacion de dominio en resultados.
- Flujo probado: Abrir Google, buscar `barranquismo granada`, navegar resultados y avanzar con `Siguiente` hasta encontrar `barranquismogranada.com`.
- Situacion observada: La sesion fue redirigida repetidamente a `https://www.google.com/sorry/index` (bloqueo anti-bot), impidiendo una paginacion normal y fiable.
- Resultado esperado: Avanzar por paginas de resultados y devolver la posicion real del dominio objetivo.
- Resultado real: `NO_ENCONTRADO max_paginas=8`, condicionado por bloqueo anti-bot antes de completar el flujo de paginacion real.
- Evidencia (artifact/log/report): `/tmp/opencode_google_paginacion.jsonl` (tool outputs con `currentUrl` en `/sorry/index` y timeout en espera de URL de resultados).
- Hipotesis de causa: Deteccion de automatizacion por Google para esta sesion/IP y bloqueo por challenge.
- Accion aplicada: Reintento en modo visible y ajuste de flujo; se mantuvo bloqueo. Ademas se implemento en la tool captura automatica en error (`autoSnapshot`) y tool de observacion intermedia (`browser_observe`) para no cortar sin evidencia.
- Validacion posterior: Validado en fixture local que ante error se adjunta `autoSnapshot` + `observation`; validado `browser_observe` con screenshot de mitad de flujo.
- Estado: en analisis
- Notas: Caso util para definir politica de handoff humano/CAPTCHA y estrategia de pruebas en buscadores con anti-bot.
### Entrada (2026-04-23 21:15)
- Contexto: Reintento de la misma prueba en Google con flujo mejorado (consentimiento + observacion intermedia + captura automatica en error).
- Flujo probado: Buscar `barranquismo granada`, avanzar por resultados y localizar `barranquismogranada.com`.
- Situacion observada: Se gestiono consentimiento (`Rechazar todo`), pero tras enviar busqueda Google redirigio a `sorry/index` (verificacion humana).
- Resultado esperado: Continuar paginacion y devolver posicion real del dominio.
- Resultado real: `BLOQUEADO_HUMANO`.
- Evidencia (artifact/log/report):
- screenshot: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-14-38-965Z-error-browser_wait.png`
- log de ejecucion: `/tmp/opencode_google_retest.jsonl`
- Hipotesis de causa: Mecanismo anti-bot de Google (challenge humano) en esta sesion/IP.
- Accion aplicada: Rechazo de cookies + reintento visible + observacion automatica.
- Validacion posterior: Pendiente de handoff humano para superar challenge y continuar la misma prueba.
- Estado: en analisis
- Notas: El nuevo mecanismo de `autoSnapshot` y `browser_observe` funciono correctamente y aporta evidencia util en mitad del flujo.
### Entrada (2026-04-23 21:23)
- Contexto: Reintento forzado de la prueba "si o si" con clic explicito de verificacion humana.
- Flujo probado: Google -> consentimiento -> busqueda -> detectar `sorry/index` -> `browser_observe` -> `browser_handle_human_check` -> reintento de salida a resultados.
- Situacion observada: La herramienta intento clic en controles de human-check, incluido intento de checkbox reCAPTCHA en iframe, pero no encontro un control clicable en esta sesion.
- Resultado esperado: Resolver challenge humano y continuar paginacion.
- Resultado real: `BLOQUEADO_HUMANO`.
- Evidencia (artifact/log/report):
- screenshot antes: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-21-49-250Z-human-check-before.png`
- screenshot despues: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-22-16-015Z-human-check-after.png`
- autosnapshot en error: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-22-11-037Z-error-browser_wait.png`
- log: `/tmp/opencode_google_humanclick_retest.jsonl`
- Hipotesis de causa: Challenge anti-bot no expone checkbox clicable directo en este contexto/IP/sesion.
- Accion aplicada: Añadida tool `browser_handle_human_check` y reintento en modo visible.
- Validacion posterior: Pendiente de handoff manual humano en la misma sesion para continuar.
- Estado: en analisis
- Notas: El sistema ya no corta sin intentar y ahora deja trazabilidad completa del intento de human-check.
### Entrada (2026-04-23 21:48)
- Contexto: Verificacion visual solicitada por usuario para confirmar si la tool realmente intenta el clic de "soy humano".
- Flujo probado: Ejecucion visible con video, captura antes/despues del human-check e intento extendido en iframes/selectores de checkbox.
- Situacion observada: Se ejecuto `browser_handle_human_check` con intentos en multiples selectores y iframes, pero no encontro control clicable (`clicked=false`).
- Resultado esperado: Clic efectivo sobre control de verificacion humana y salida de `sorry/index`.
- Resultado real: Sin clic efectivo; permanencia en `sorry/index`.
- Evidencia (artifact/log/report):
- `before`: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-47-40-040Z-before-human-check-v2.png`
- `after`: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T21-48-04-615Z-after-human-check-v2.png`
- video: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@980084a2685562a3f2fba478b8730d98.webm`
- log: `/tmp/opencode_google_watch_click_v2.jsonl`
- Hipotesis de causa: El challenge mostrado en esta sesion no expone checkbox clicable automatizable con los selectores actuales.
- Accion aplicada: Ampliacion de estrategia de clic (iframes + selectors checkbox + fallback por texto).
- Validacion posterior: Confirmado por salida de tool (`clicked=false`) y evidencia visual.
- Estado: en analisis
- Notas: Para completar esta prueba en este entorno, se requiere handoff manual sobre el challenge actual.
### Entrada (2026-04-24 00:18)
- Contexto: Validacion pedida por usuario para comprobar movimiento visible de cursor y scroll real con contenido suficiente.
- Flujo probado: Abrir fixture largo local -> verificar cursor overlay -> hover -> scroll de pagina -> scroll de contenedor -> snapshot/video.
- Situacion observada: Con fixture anterior no habia altura suficiente para probar scroll; se creo fixture nuevo con contenido largo y contenedor interno desplazable.
- Resultado esperado: Evidenciar que el cursor virtual existe y que el desplazamiento cambia posiciones reales de scroll.
- Resultado real: Cursor overlay presente (`#__browser_tool_cursor`) y scroll efectivo (`movedY=1200` en pagina, `movedY=420` en `#scroll-box`).
- Evidencia (artifact/log/report):
- fixture: `opencode-browser-tool/scripts/fixture_scroll_long.html`
- screenshot: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-18-49-353Z-cursor-scroll-smoke.png`
- video: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@b0a5c648845bfba94a0843fd81d664d9.webm`
- Hipotesis de causa: La ausencia de scroll en pruebas previas se debia al contenido insuficiente de la pagina de test, no a fallo de `browser_scroll`.
- Accion aplicada: Se anadio fixture largo y se enriquecio salida de `browser_scroll` con metricas (`startY/endY/movedY/maxY`).
- Validacion posterior: Build OK y smoke visual OK.
- Estado: resuelto
- Notas: Queda base estable para validar scroll en nuevas regresiones.
### Entrada (2026-04-24 00:19)
- Contexto: Retest de Google tras ampliar estrategia de `browser_handle_human_check`.
- Flujo probado: Google -> consentimiento reject -> busqueda `barranquismo granada` -> `sorry/index` -> intento human-check -> observacion antes/despues.
- Situacion observada: La tool reporto clic efectivo en ancla reCAPTCHA dentro de iframe, pero la URL continuo en `sorry/index` en este intento.
- Resultado esperado: Superar challenge y volver a `/search` para continuar paginacion.
- Resultado real: `clicked=true` con metodo `frame:iframe[title*='reCAPTCHA']:#recaptcha-anchor`, sin salida inmediata de `sorry/index`.
- Evidencia (artifact/log/report):
- before: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-19-11-410Z-google-before-human-v3.png`
- after: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-19-13-806Z-google-after-human-v3.png`
- video: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@02542037c2003982eb2da9641c77f00e.webm`
- Hipotesis de causa: El challenge requiere pasos adicionales (puzzle/espera/validacion servidor) tras el clic inicial, o decision por reputacion IP/sesion.
- Accion aplicada: Se amplio `handleHumanCheck` con mas selectores y fallback por coordenadas; se confirmo clic efectivo al menos en una variante.
- Validacion posterior: Pendiente retest con handoff manual para completar salida a `/search` y continuar paginacion.
- Estado: en analisis
- Notas: Se cierra duda previa de "si la tool clickea o no": ahora hay evidencia de clic efectivo.
### Entrada (2026-04-24 00:22)
- Contexto: Reintento de la prueba objetivo completa (Google + paginacion) usando script de regresion v4.
- Flujo probado: Google -> reject consent -> buscar `barranquismo granada` -> human-check -> intentar paginacion hasta 8 paginas.
- Situacion observada: `handleHumanCheck` volvio a reportar clic efectivo en reCAPTCHA, pero la sesion se mantuvo en `sorry/index`, impidiendo entrar en `/search` y por tanto sin paginacion real.
- Resultado esperado: Salir a resultados de busqueda y avanzar paginas hasta encontrar `barranquismogranada.com`.
- Resultado real: `found=false` con bloqueo persistente en `sorry/index`.
- Evidencia (artifact/log/report):
- screenshot final: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-22-24-694Z-google-pagination-v4-final.png`
- video: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@4d214e591bd81c6d279e36fa186d5e0b.webm`
- script usado: `opencode-browser-tool/scripts/google_pagination_v4.mjs`
- Hipotesis de causa: El challenge requiere validacion adicional no resuelta por el primer clic (puzzle/espera/verificacion servidor) o mantiene bloqueo por reputacion de sesion/IP.
- Accion aplicada: Ejecutado retest v4 completo tras mejoras de clic en human-check.
- Validacion posterior: Confirmado que el clic se intenta y ocurre (`clicked=true`), pero no desbloquea en este entorno.
- Estado: en analisis
- Notas: Para completar esta prueba concreta en este entorno sigue siendo necesario handoff manual directo al challenge.
### Entrada (2026-04-24 21:10)
- Contexto: Repeticion de prueba de ranking organico tras habilitar modo de perfil persistente y tras login manual previo del usuario.
- Flujo probado: Google -> aceptar cookies -> buscar `barranquismo granada` -> resolver posible `sorry/index` -> paginar resultados organicos.
- Situacion observada: Persistio redireccion inicial a `sorry/index`, pero el flujo continuo a `/search` y se pudo completar paginacion organica.
- Resultado esperado: Obtener posicion del dominio objetivo en resultados organicos.
- Resultado real: Dominio encontrado en pagina 2, posicion 6 organica (rank global 16 en conteo del script).
- Evidencia (artifact/log/report):
- before manual: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-10-05-886Z-google-v7-before-manual.png`
- after manual: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-10-31-757Z-google-v7-after-manual.png`
- final: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-10-39-450Z-google-v7-final.png`
- Hipotesis de causa: El historial/perfil ayuda, pero no elimina completamente el challenge anti-bot en este entorno.
- Accion aplicada: Ejecucion con `browserKind=testing` + `persistentProfile=true` + `userDataDir=/home/pancho/.chromium-perfil-google`.
- Validacion posterior: Flujo de ranking organico completado y posicion devuelta.
- Estado: resuelto
- Notas: Siguiente comparativa recomendada en este mismo proyecto: repetir con `browserKind=system` + perfil persistente equivalente.
### Entrada (2026-04-24 21:28)
- Contexto: Prueba solicitada en `google.es` con foco en resultados organicos, clic al resultado encontrado y scroll suave en la web destino.
- Flujo probado: abrir Google -> buscar `barranquismo granada` -> localizar primer organico con dominio `barranquismogranada.com` -> clic -> scroll progresivo hasta abajo.
- Situacion observada: La sesion entro directamente a resultados (`/search`) sin bloqueo `sorry/index` en esta corrida.
- Resultado esperado: Obtener posicion organica, abrir el resultado y completar scroll hasta el final.
- Resultado real: Dominio encontrado en pagina 2, posicion 7 organica (rank global 17); clic exitoso y scroll completo (`loops=9`, `lastMoved=0`).
- Evidencia (artifact/log/report):
- destino bottom: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-28-57-330Z-google-v8d-destination-bottom.png`
- final: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-28-58-023Z-google-v8d-final.png`
- script: `opencode-browser-tool/scripts/google_es_v8d.mjs`
- Hipotesis de causa: El perfil persistente aporta estabilidad de sesion para este caso concreto, aunque la posicion organica puede variar por momento/indice localizacion.
- Accion aplicada: Se limitaron esperas extra a maximo 3s en tramo de challenge y se ejecuto scroll por pasos suaves.
- Validacion posterior: Flujo completo terminado correctamente.
- Estado: resuelto
- Notas: Se confirma comportamiento solicitado (clic visible + scroll visible + conteo organico).
### Entrada (2026-04-24 22:10)
- Contexto: Prueba solicitada con nueva query en Google: `puertas cortafuegos de madera`.
- Flujo probado: abrir `google.es` -> buscar query -> paginar resultados -> localizar dominio `puertastecnicasbcn.com` -> navegar al resultado -> snapshot.
- Situacion observada: La navegacion inicial en `google.es` acaba sirviendo resultados en host `google.com` en esta corrida, manteniendo idioma/contexto ES.
- Resultado esperado: obtener posicion del dominio y capturar la pagina destino.
- Resultado real: dominio encontrado en pagina 3, posicion 4 organica (rank global 23), URL `https://www.puertastecnicasbcn.com/puertas-cortafuegos/`.
- Evidencia (artifact/log/report):
- snapshot destino: `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T20-10-49-705Z-google-puertas-destination.png`
- script: `opencode-browser-tool/scripts/google_es_puertas_rank.mjs`
- Hipotesis de causa: Variacion normal de SERP por datacenter/sesion/ubicacion.
- Accion aplicada: paginacion automatica + click al resultado encontrado + snapshot final.
- Validacion posterior: prueba completada sin bloqueo de challenge en esta corrida.
- Estado: resuelto
- Notas: Playwright `page.screenshot` captura viewport de la pagina, no la barra de URL del navegador.

114
docs/TODO.md Normal file
View file

@ -0,0 +1,114 @@
# TODO - opencode-browser-tool
## Objetivo del TODO
Recoger el trabajo completo necesario desde el estado actual hasta un producto terminado y utilizable al 100 por 100.
---
## Fase 0 - Base del proyecto
- [x] Confirmar y congelar alcance exacto de la v1
- [x] Confirmar stack tecnico definitivo para la v1 (`Node.js/TypeScript`, `Playwright`, `Chromium`, `MCP`)
- [x] Definir estructura inicial de carpetas del proyecto
- [x] Crear `README.md` principal del proyecto
- [x] Crear scripts base de instalacion y verificacion
---
## Fase 1 - Integracion minima funcional con OpenCode
- [x] Crear el `MCP server` externo del proyecto
- [x] Definir herramientas MCP minimas de la v1
- [x] Conectar el `MCP server` con `Playwright`
- [x] Lanzar `Chromium` en modo visible por defecto
- [x] Validar que OpenCode detecta y usa la herramienta
- [x] Probar primera navegacion real contra una URL local
---
## Fase 2 - Interaccion efectiva con la UI
- [x] Implementar acciones base: abrir URL, click, type, press, select, hover y scroll
- [x] Implementar esperas utiles para estabilidad de pruebas
- [x] Implementar lectura de DOM, texto visible y estado de elementos
- [x] Permitir ejecucion de JavaScript controlado en pagina
- [x] Definir respuesta estructurada para cada accion
- [x] Probar flujos reales sobre una app de desarrollo local
---
## Fase 3 - Evidencia y diagnostico inicial
- [x] Implementar screenshots
- [x] Capturar logs de consola
- [x] Capturar errores de pagina
- [x] Definir almacenamiento de artefactos por ejecucion
- [x] Evaluar inclusion de video o trace en la v1
- [x] Diseñar formato de reporte util para el agente
---
## Fase 4 - Instalacion y empaquetado
- [x] Crear `install.sh`
- [x] Crear `check.sh`
- [x] Crear plantilla de configuracion MCP para OpenCode
- [x] Documentar instalacion paso a paso en un `.md` (`docs/QUICKSTART.md`)
- [ ] Verificar instalacion en un entorno limpio con OpenCode (checklist en `docs/VALIDACION_ENTORNO_LIMPIO.md`)
- [x] Asegurar que todo el proyecto se despliega desde una unica carpeta
- [ ] Cerrar instalable Linux sin rutas hardcodeadas (proyecto, perfil, OpenCode)
- [ ] Alinear `install.sh` y `check.sh` con puesta en marcha en PC nuevo
- [ ] Cerrar plantilla MCP portable para rutas locales variables
---
## Fase 5 - Robustez de la v1
- [ ] Mejorar manejo de errores y mensajes devueltos al agente
- [ ] Definir gestion minima de sesiones de navegador
- [ ] Definir rutas y limpieza de artefactos
- [ ] Probar estabilidad en varias ejecuciones consecutivas
- [ ] Documentar limitaciones conocidas de la v1
- [ ] Cerrar criterios de aceptacion de la v1
---
## Fase 6 - V2 preparada para continuidad
- [ ] Diseñar backend/orquestador intermedio
- [ ] Diseñar contratos estables entre MCP y runner interno
- [ ] Diseñar gestion mas rica de perfiles y sesiones
- [ ] Añadir puntos de extension para ganchos `CDP`
- [ ] Preparar base para politicas de seguridad en webs externas
- [ ] Evaluar soporte futuro de `WebKit`
---
## Fase 7 - Producto terminado al 100 por 100
- [ ] Cerrar instalacion reproducible en una maquina nueva con OpenCode
- [ ] Cerrar experiencia de uso completa para agente y usuario
- [ ] Cerrar documentacion final de uso, instalacion y mantenimiento
- [ ] Probar apps locales reales con flujos completos
- [ ] Probar casos externos autorizados cuando llegue el momento
- [ ] Dejar lista la estrategia de evolucion y mantenimiento del proyecto
---
## Pendientes inmediatos acordados
- [ ] Ejecutar prueba funcional final acordada (smoke de aceptacion)
- [ ] Ejecutar plan de cierre de instalable segun `docs/PLAN_CIERRE_INSTALABLE.md`
- [ ] Evaluar complejidad de soporte Windows y viabilidad de flujo neutro por `npm`/Node
---
## Registro de decisiones pendientes
- [ ] Politica exacta para webs externas
- [ ] Allowlist de dominios
- [ ] Confirmacion previa para acciones sensibles
- [ ] Gestion de `CAPTCHA` y `2FA`
- [ ] Persistencia o limpieza de sesion entre ejecuciones
- [ ] Estrategia final `managed chromium` vs `system browser`

View file

@ -0,0 +1,93 @@
# Validacion en entorno limpio (OpenCode)
Objetivo: verificar que `opencode-browser-tool` puede instalarse y usarse desde cero en una maquina con OpenCode.
## Alcance de la validacion
- Instalacion de dependencias y Chromium gestionado por Playwright.
- Compilacion del servidor MCP.
- Conexion MCP visible para OpenCode.
- Ejecucion de una prueba minima end-to-end.
## Prerrequisitos
- Node.js 20+ y npm.
- OpenCode CLI instalado y funcional.
- Acceso a internet para instalar dependencias y Chromium.
## Pasos
1) Copiar el proyecto completo en una carpeta local, por ejemplo:
```bash
cp -R opencode-browser-tool "$HOME/opencode-browser-tool"
```
2) Instalar:
```bash
cd "$HOME/opencode-browser-tool"
./install.sh
```
3) Verificacion basica:
```bash
npm run check
npm run build
```
4) Configurar MCP para OpenCode con la ruta local real de `dist/server.js`:
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"browser-tool": {
"type": "local",
"command": [
"node",
"/ABSOLUTE/PATH/opencode-browser-tool/dist/server.js"
]
}
}
}
```
5) Comprobar que OpenCode detecta el MCP:
```bash
opencode mcp list
```
Resultado esperado: `browser-tool` aparece como `connected`.
6) Prueba minima desde OpenCode:
- abrir navegador visible
- navegar a `https://example.com`
- tomar snapshot con label `clean-env-smoke`
- cerrar navegador
7) Verificar artifacts:
- Debe existir una captura en `artifacts/`.
## Criterios de aceptacion
- `install.sh` termina sin error.
- `npm run build` termina sin error.
- `opencode mcp list` muestra `browser-tool` conectado.
- La prueba minima produce screenshot en `artifacts/`.
## Resultado de la ejecucion
Rellenar al completar la validacion real:
- Fecha:
- Equipo/SO:
- Version Node:
- Version OpenCode:
- Resultado: pendiente | ok | fallo
- Evidencia (capturas/logs):
- Notas:

View file

@ -0,0 +1,68 @@
# Contexto activo - Browser Tool
Este archivo guarda el estado operativo que NO debe perderse por compactacion.
## Objetivo activo
Completar la prueba concreta en Google para el flujo:
- buscar `barranquismo granada`
- superar `sorry/index` cuando aparezca
- continuar paginacion
- localizar `barranquismogranada.com` y devolver posicion real
## Estado actual real
- Se mejoro `browser_handle_human_check` con estrategia ampliada (iframes, selectores extra y clic por coordenadas como fallback).
- En un retest reciente, la tool reporto clic efectivo:
- `clicked: true`
- `method: frame:iframe[title*='reCAPTCHA']:#recaptcha-anchor`
- Aun con clic detectado, la URL siguio en `sorry/index` en los retests v3 y v4.
- En retest v7 con perfil persistente (`/home/pancho/.chromium-perfil-google`) el challenge inicial aparecio, pero se logro continuar a `/search` y completar ranking organico.
- Se implemento verbose por defecto (`verbose=true`, `verboseOverlay=true`) y delay humano configurable 1-3s en interacciones.
- Se implemento guarda de estabilidad previa a pasos sensibles para reducir errores tipo `Execution context was destroyed`.
## Evidencia clave
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-19-11-410Z-google-before-human-v3.png`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-19-13-806Z-google-after-human-v3.png`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@02542037c2003982eb2da9641c77f00e.webm`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-22-24-694Z-google-pagination-v4-final.png`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@4d214e591bd81c6d279e36fa186d5e0b.webm`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-24T19-10-39-450Z-google-v7-final.png`
## Cursor y scroll (peticion explicita del usuario)
- Se creo fixture largo para validar scroll real:
- `opencode-browser-tool/scripts/fixture_scroll_long.html`
- Retest de smoke (visible) confirmado:
- cursor virtual presente (`#__browser_tool_cursor` existe)
- scroll de pagina: `movedY=1200`
- scroll de contenedor `#scroll-box`: `movedY=420`
Evidencia:
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/2026-04-23T22-18-49-353Z-cursor-scroll-smoke.png`
- `/home/pancho/Documentos/Empresa/IA/opencode-browser-tool/artifacts/page@b0a5c648845bfba94a0843fd81d664d9.webm`
## Siguiente paso exacto (sin desviar foco)
1. Ejecutar una prueba funcional final acordada (smoke de aceptacion).
2. Cerrar instalable Linux portable (sin rutas hardcodeadas) usando `opencode-browser-tool/docs/PLAN_CIERRE_INSTALABLE.md`.
3. Validar instalacion en entorno limpio con OpenCode.
4. Evaluar complejidad de soporte Windows y opcion cross-platform via scripts npm/Node.
## Plan de cierre preservado
- Documento fuente: `opencode-browser-tool/docs/PLAN_CIERRE_INSTALABLE.md`
- Este plan debe conservarse y ejecutarse despues de la prueba funcional final.
## Regla de continuidad
Antes de iniciar cualquier trabajo nuevo relacionado con browser-tool, leer:
- `docs/CONTEXTO_ACTIVO_BROWSER.md`
- `docs/HISTORIAL_SESIONES.md`
- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md`
No cerrar ni reemplazar este archivo: actualizarlo incrementalmente.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
# Skill local de continuidad (manual)
Nota: en este entorno no hay skills cargables del sistema.
Este documento funciona como skill persistente manual para que cualquier agente retome exactamente donde se quedo el trabajo.
## Prompt recomendado para arranque de agente
Usar este texto al iniciar una sesion nueva:
```text
Actua como Agente modulo Browser Opencode.
Antes de responder, lee y usa como fuente de verdad:
- docs/CONTEXTO_ACTIVO_BROWSER.md
- docs/HISTORIAL_SESIONES.md
- opencode-browser-tool/docs/REGISTRO_SITUACIONES.md
Objetivo: continuar exactamente el trabajo activo, sin desviarte a tareas nuevas.
Si hay un bloqueo, primero deja evidencia (observe/snapshot/video), luego propone o aplica el siguiente paso minimo.
Cada accion relevante debe quedar registrada en docs/HISTORIAL_SESIONES.md.
```
## Checklist minima de continuidad
- Confirmar `session_id` actual.
- Revisar ultimo estado en `docs/CONTEXTO_ACTIVO_BROWSER.md`.
- Ejecutar solo el siguiente paso exacto definido en ese archivo.
- Registrar cambios y evidencia.

View file

@ -0,0 +1,120 @@
# Herramienta Browser para OpenCode: idea y funcionamiento
## Proposito
Este documento recoge ideas, acuerdos y criterios sobre la futura herramienta browser para OpenCode.
Su objetivo es servir como espacio vivo de trabajo mientras seguimos conversando sobre posibilidades, tecnologias, formas de implementacion y decisiones funcionales, antes de pasar a fases de diseno tecnico o construccion detallada.
---
## Idea general de la herramienta
La herramienta permitira que OpenCode pueda usar un navegador para probar proyectos del workspace, moviendose por la aplicacion e interactuando con ella de forma similar a como lo haria un usuario real.
La referencia funcional buscada es un comportamiento parecido al de Antigravity: navegar por la app, ejecutar flujos, interactuar con la interfaz y comprobar si la funcionalidad desarrollada responde como se espera.
---
## Vision funcional inicial
Cuando la herramienta este hecha, se espera que pueda:
- abrir la aplicacion objetivo en un navegador controlado
- recorrer pantallas y flujos reales
- hacer acciones de usuario como clic, escritura, seleccion, scroll y navegacion
- comprobar resultados esperados en cada paso
- recoger evidencia util cuando algo falle o cuando una comprobacion deba quedar registrada
- devolver al agente informacion util sobre lo ocurrido durante la prueba
---
## Modos de ejecucion
### Visible como modo por defecto
El modo visible sera el modo principal de uso de la herramienta.
Se considera el modo mas adecuado cuando lo que se quiere comprobar esta ligado a la interaccion real con la aplicacion y a la experiencia de uso:
- comportamiento de la interfaz
- navegacion entre pantallas
- interacciones propias de usuario
- validacion funcional de flujos completos
- observacion directa de lo que ocurre en pantalla
### Headless como modo no principal
El modo headless no sera el comportamiento por defecto.
Se utilizara cuando lo que se quiera comprobar no dependa principalmente de la interaccion visible de usuario, sino del comportamiento interno o tecnico del sistema, por ejemplo:
- comunicacion con backend
- entradas y salidas de informacion
- interacciones con disco, almacenamiento o estado interno
- validaciones tecnicas no centradas en UX
---
## Forma de uso esperada
- El agente podra usar esta herramienta igual que usa el resto de herramientas, cuando la necesite para resolver la tarea que este realizando.
- El usuario tambien podra pedir expresamente al agente que use la herramienta browser para una tarea concreta.
- Debe existir una forma de activar o desactivar su uso segun prefiera el usuario.
- El agente debera respetar esa configuracion para no usar la herramienta cuando el usuario no quiera que intervenga.
---
## Criterio estructural principal
La herramienta debe construirse como una solucion externa a OpenCode.
Esto implica que tanto el browser como su forma de instalacion, ejecucion e instrucciones de uso para el agente deben quedar fuera del nucleo interno de OpenCode, para que una actualizacion de OpenCode no afecte al funcionamiento de esta herramienta.
El objetivo es que la integracion sea estable, desacoplada y mantenible, evitando depender de modificaciones internas del proyecto OpenCode.
---
## Telemetria en tiempo real (fase posterior)
En la version actual, la herramienta funciona con un modelo request/response por accion:
- el agente invoca una tool
- la tool ejecuta
- la tool devuelve resultado estructurado
Con lo ya implementado (diagnostico, artifacts, reportes), este modelo cubre bien el uso principal en desarrollo.
### Que aportaria incluir telemetria en tiempo real mas adelante
- stream en vivo de eventos de ejecucion (consola, red, errores, cambios de estado)
- observacion continua sin esperar al fin de cada accion
- mejor soporte para casos intermitentes o muy complejos
- mejor operacion para soporte de clientes cuando haya que mirar una sesion en directo
### Que haria falta para incluirla
- un servicio paralelo local (sidecar) separado del MCP principal
- un canal de eventos push (`WebSocket` o `SSE`)
- un esquema de eventos estandar para pasos, diagnostico y estado
### Decision actual
- no incluir telemetria en tiempo real en esta version
- mantenerla documentada como mejora de continuidad para fases posteriores
- priorizar primero estabilidad funcional, diagnostico estructurado y reporte util para desarrollo
---
## Por definir
- si `external_web` debe estar habilitado o deshabilitado por defecto
- si desde la v1 debe existir allowlist de dominios permitidos
- si las acciones sensibles deben requerir confirmacion explicita del usuario
- como se gestionaran `CAPTCHA`, `2FA` o pasos que requieran intervencion manual
---
## Estado del documento
Documento inicial abierto para seguir recogiendo ideas y acuerdos antes de entrar en diseno tecnico, arquitectura o implementacion.

View file

@ -0,0 +1,686 @@
# Manual de uso de la herramienta Browser
Este manual explica que tools expone la herramienta browser, como usarlas y que puedes pedirle a OpenCode en la practica.
## Alcance actual
- Browser: `Chromium` controlado por `Playwright`
- Integracion: `MCP` por `stdio`
- Modo por defecto: visible (`headless: false`)
- Artefactos: capturas en `opencode-browser-tool/artifacts/`
## Esquema de respuesta estandar
Todas las tools devuelven ahora un formato unificado.
Respuesta OK:
```json
{
"ok": true,
"tool": "browser_<tool>",
"data": {},
"state": {
"isOpen": true,
"currentUrl": "https://..."
}
}
```
Respuesta con error:
```json
{
"ok": false,
"tool": "browser_<tool>",
"error": {
"code": "TOOL_EXECUTION_ERROR",
"message": "detalle del error"
},
"state": {
"isOpen": false,
"currentUrl": null
}
}
```
## Flujo recomendado de uso
1. Abrir sesion de navegador con `browser_open`
2. Navegar con `browser_navigate`
3. Interactuar con `browser_click`, `browser_type`, `browser_press`, `browser_scroll`
4. Esperar estados estables con `browser_wait`
5. Capturar evidencia con `browser_snapshot`
6. Inspeccionar estado dinamico con `browser_evaluate`
7. Cerrar sesion con `browser_close`
Importante: todas las tools excepto `browser_open` y `browser_close` requieren sesion abierta. Si no hay sesion, devuelve error.
## Espera automatica nativa (comportamiento actual)
La herramienta ya incorpora estabilizacion automatica despues de acciones clave.
Que significa en la practica:
- despues de `browser_navigate`, `browser_click`, `browser_type`, `browser_press`, `browser_scroll` y `browser_evaluate`, la herramienta intenta esperar estado estable por si sola
- combina `domcontentloaded`, intento de `networkidle` y una ventana de "quietud" de actividad de red
- si no logra estado estable, aplica salida segura por tiempo maximo para evitar bucles de espera infinita
Resultado:
- normalmente no necesitas pedir `wait` manual para casos comunes
- `browser_wait` sigue disponible cuando quieras una condicion explicita (selector/texto/url/timeout)
---
## Tools disponibles
### `browser_help`
Devuelve un resumen operativo de la herramienta: capacidades, defaults, notas y ejemplos.
Cuando usarla:
- al empezar una sesion nueva con un agente que no conoce la tool
- para recordar comportamiento por defecto (modo visible, timeouts, auto-wait)
Parametros: ninguno.
Ejemplo:
```json
{}
```
---
### `browser_health`
Devuelve estado operativo actual del runtime browser y artifacts recientes.
Incluye:
- estado de sesion (`isOpen`, `currentUrl`)
- actividad de red interna (`inflight`, `lastActivityAgoMs`)
- comportamiento activo (`verbose`, overlay, delay humano)
- pasos recientes (`recentSteps`) para diagnostico rapido en vivo
- lista de artifacts recientes (ruta, fecha, tamano)
Parametros:
- `limit` (number, opcional): cantidad maxima de artifacts a devolver (default `5`, max `20`)
Ejemplo:
```json
{
"limit": 5
}
```
---
### `browser_config`
Permite leer o cambiar configuracion persistente de la herramienta (guardada en `config/browser-tool.config.json`).
Parametros:
- `action` (string, opcional): `get` o `set` (default `get`)
- `browserDefaultKind` (string, opcional): `testing` o `system`
- `browserSystemExecutablePath` (string, opcional): ruta del navegador del sistema (ej. `/usr/bin/google-chrome`)
- `browserDefaultPersistentProfile` (boolean, opcional): si `true`, `browser_open` usa perfil persistente por defecto
- `browserDefaultUserDataDir` (string, opcional): ruta de perfil persistente por defecto
- `browserDefaultVerbose` (boolean, opcional): modo verbose por defecto (`true` recomendado para operacion)
- `browserDefaultVerboseOverlay` (boolean, opcional): overlay visual de estado en navegador visible
- `browserDefaultInteractionDelayMinMs` (number, opcional): minimo de delay humano en acciones interactivas
- `browserDefaultInteractionDelayMaxMs` (number, opcional): maximo de delay humano en acciones interactivas
- `reportDefaultSaveToFile` (boolean, opcional): default persistente de `browser_report.saveToFile`
- `reportDefaultFormat` (string, opcional): `json`, `markdown`, `both`
Ejemplo lectura:
```json
{
"action": "get"
}
```
Ejemplo cambio persistente:
```json
{
"action": "set",
"browserDefaultKind": "system",
"browserSystemExecutablePath": "/usr/bin/google-chrome",
"browserDefaultPersistentProfile": true,
"browserDefaultUserDataDir": "/home/pancho/.chrome-perfil-google-real",
"browserDefaultVerbose": true,
"browserDefaultVerboseOverlay": true,
"browserDefaultInteractionDelayMinMs": 1000,
"browserDefaultInteractionDelayMaxMs": 3000,
"reportDefaultSaveToFile": false,
"reportDefaultFormat": "json"
}
```
---
### `browser_diagnostics`
Devuelve eventos recientes de diagnostico capturados durante la sesion:
- `console` (log, warn, error, etc.)
- `pageerror` (errores JS no capturados)
- `requestfailed` (peticiones de red fallidas)
Parametros:
- `limit` (number, opcional): maximo de eventos devueltos (default `20`, max `200`)
Ejemplo:
```json
{
"limit": 20
}
```
---
### `browser_diagnostics_clear`
Limpia el buffer interno de diagnostico.
Uso recomendado: antes de arrancar una prueba nueva para leer solo eventos de ese flujo.
Parametros: ninguno.
```json
{}
```
---
### `browser_observe`
Toma una "foto de estado" de la pagina durante el flujo y, opcionalmente, una captura.
Uso recomendado:
- cuando el flujo parece bloqueado
- cuando no avanza tras una accion
- antes de abortar una prueba para entender que esta viendo el navegador
Parametros:
- `screenshot` (boolean, opcional, default `true`)
- `label` (string, opcional)
Ejemplo:
```json
{
"screenshot": true,
"label": "mid-flow-check"
}
```
---
### `browser_handle_consent`
Intenta pulsar botones comunes de consentimiento.
Parametros:
- `action` (string, opcional): `reject` (default) o `accept`
Ejemplo:
```json
{
"action": "reject"
}
```
---
### `browser_handle_human_check`
Intenta resolver controles comunes de verificacion humana (incluyendo checkbox de reCAPTCHA cuando es clicable).
Estrategia actual:
- intenta clic en checkboxs dentro de iframes comunes de reCAPTCHA
- intenta selectores de captcha tambien en pagina principal
- usa fallback por coordenadas para controles detectados por texto
Uso recomendado:
- cuando aparece bloqueo tipo `sorry/index`
- cuando el flujo queda detenido en "comprueba que eres humano"
Parametros: ninguno.
```json
{}
```
---
### `browser_open`
Abre una sesion de `Chromium`.
Parametros:
- `headless` (boolean, opcional): por defecto `false`
- `startUrl` (string, opcional)
- `width` (number, opcional, default `1440`)
- `height` (number, opcional, default `900`)
- `browserKind` (string, opcional): `testing` (Playwright managed) o `system` (browser del sistema)
- `executablePath` (string, opcional): ruta binaria explicita del navegador
- `persistentProfile` (boolean, opcional): activar perfil persistente
- `userDataDir` (string, opcional): ruta del perfil persistente (requerido si `persistentProfile=true`)
- `verbose` (boolean, opcional, default `true`): muestra estado paso a paso en respuestas
- `verboseOverlay` (boolean, opcional, default `true`): muestra estado en overlay visual en navegador visible
- `interactionDelayMinMs` (number, opcional, default `1000`)
- `interactionDelayMaxMs` (number, opcional, default `3000`)
- `recordVideo` (boolean, opcional, default `false`)
- `recordTrace` (boolean, opcional, default `false`)
- `recordLabel` (string, opcional, default `session`)
Ejemplo:
```json
{
"headless": false,
"startUrl": "http://localhost:3000",
"width": 1440,
"height": 900,
"browserKind": "system",
"persistentProfile": true,
"userDataDir": "/home/pancho/.chrome-perfil-google-real",
"verbose": true,
"verboseOverlay": true,
"interactionDelayMinMs": 1000,
"interactionDelayMaxMs": 3000,
"recordVideo": true,
"recordTrace": true,
"recordLabel": "case-123"
}
```
Notas:
- `recordVideo` sigue desactivado por defecto para evitar consumo innecesario.
- si activas `recordVideo` o `recordTrace`, los artifacts se generan al cerrar sesion con `browser_close`.
- con `verbose=true`, la tool muestra estado actual y aplica guardas de estabilidad antes de pasos sensibles.
---
### `browser_close`
Cierra la sesion activa de navegador.
Parametros: ninguno.
Ejemplo:
```json
{}
```
---
### `browser_navigate`
Navega la pagina activa a una URL.
Parametros:
- `url` (string, requerido)
- `waitUntil` (string, opcional): `load`, `domcontentloaded`, `networkidle` (default `domcontentloaded`)
Ejemplo:
```json
{
"url": "http://localhost:3000/login",
"waitUntil": "domcontentloaded"
}
```
---
### `browser_click`
Hace click sobre un selector CSS.
Nota: antes del click intenta mover el puntero al centro del elemento para hacer la interaccion mas natural.
Parametros:
- `selector` (string, requerido)
- `timeoutMs` (number, opcional, default `10000`)
Ejemplo:
```json
{
"selector": "button[type='submit']",
"timeoutMs": 10000
}
```
---
### `browser_hover`
Hace hover sobre un elemento por selector.
Parametros:
- `selector` (string, requerido)
- `timeoutMs` (number, opcional, default `10000`)
Ejemplo:
```json
{
"selector": ".menu-item-settings",
"timeoutMs": 10000
}
```
---
### `browser_type`
Escribe texto en un elemento por selector.
Nota: antes de escribir intenta mover puntero y enfocar el campo con click.
Parametros:
- `selector` (string, requerido)
- `text` (string, requerido)
- `clear` (boolean, opcional, default `true`)
- `timeoutMs` (number, opcional, default `10000`)
Ejemplo:
```json
{
"selector": "input[name='email']",
"text": "usuario@dominio.com",
"clear": true,
"timeoutMs": 10000
}
```
---
### `browser_select`
Selecciona una opcion en un elemento `<select>` por valor.
Parametros:
- `selector` (string, requerido)
- `value` (string, requerido)
- `timeoutMs` (number, opcional, default `10000`)
Ejemplo:
```json
{
"selector": "#tipoConsentimiento",
"value": "quirurgico",
"timeoutMs": 10000
}
```
---
### `browser_press`
Envia una tecla al contexto de teclado de la pagina.
Parametros:
- `key` (string, requerido). Ejemplos: `Enter`, `Escape`, `Tab`, `Control+A`.
Ejemplo:
```json
{
"key": "Enter"
}
```
---
### `browser_scroll`
Hace scroll en pagina completa o dentro de un elemento.
Parametros:
- `x` (number, opcional, default `0`)
- `y` (number, opcional, default `400`)
- `selector` (string, opcional): si se envia, el scroll se aplica al elemento
Nota: cuando el scroll es de pagina, se aplica en varios pasos cortos para que el desplazamiento sea visible y progresivo.
Si no hay desplazamiento por rueda, aplica fallback `window.scrollBy`.
La salida incluye metricas utiles:
- `startY`, `endY`, `maxY`, `movedY` para scroll de pagina
- `movedX`, `movedY` para scroll de elemento
Ejemplo pagina:
```json
{
"y": 700
}
```
Ejemplo elemento:
```json
{
"selector": ".lista-scroll",
"y": 500
}
```
---
### `browser_wait`
Espera a que se cumpla una condicion.
Nota: en esta version es opcional para muchos flujos, porque la herramienta ya aplica espera automatica nativa tras acciones comunes.
Parametros:
- `for` (string, requerido): `selector`, `text`, `url`, `timeout`
- `value` (string, requerido para `selector`, `text`, `url`)
- `timeoutMs` (number, opcional, default `10000`)
Nota para `for: timeout`:
- si envias `value`, se interpreta como milisegundos de espera
- si no envias `value`, usa `timeoutMs`
Ejemplo por selector:
```json
{
"for": "selector",
"value": ".toast-success",
"timeoutMs": 10000
}
```
Ejemplo por tiempo:
```json
{
"for": "timeout",
"timeoutMs": 1500
}
```
---
### `browser_snapshot`
Toma screenshot y lo guarda en `artifacts/`.
Parametros:
- `label` (string, opcional)
- `fullPage` (boolean, opcional, default `true`)
Ejemplo:
```json
{
"label": "post-login",
"fullPage": true
}
```
---
### `browser_evaluate`
Ejecuta JavaScript en la pagina. Puede recibir una funcion en texto (con `arg`) o una expresion directa.
Importante: si, puede inyectar y ejecutar logica JavaScript en runtime. Eso incluye leer variables del DOM y tambien modificarlas temporalmente dentro de la sesion de prueba.
Parametros:
- `script` (string, requerido): funcion JS serializada
- `arg` (object, opcional): dato de entrada a la funcion
Ejemplo:
```json
{
"script": "(arg) => ({ title: document.title, arg })",
"arg": { "source": "qa" }
}
```
Ejemplo con expresion directa:
```json
{
"script": "document.title"
}
```
---
### `browser_query`
Lee estado DOM/UI por selector sin tener que escribir JavaScript para casos comunes.
Parametros:
- `selector` (string, requerido)
- `mode` (string, opcional): `text`, `html`, `value`, `exists`, `visible`, `enabled`, `count`, `attributes`
Ejemplos:
```json
{
"selector": "#email",
"mode": "value"
}
```
```json
{
"selector": ".toast-success",
"mode": "exists"
}
```
---
### `browser_report`
Genera un reporte consolidado de ejecucion a partir de pasos, diagnostico y artifacts.
Parametros:
- `format` (string, opcional): `json`, `markdown`, `both` (default `both`)
- `includeSteps` (boolean, opcional, default `true`)
- `includeDiagnostics` (boolean, opcional, default `true`)
- `includeArtifacts` (boolean, opcional, default `true`)
- `saveToFile` (boolean, opcional): por defecto usa configuracion persistente (`true` al instalar)
- `label` (string, opcional, default `report`)
- `limitSteps` (number, opcional, default `200`)
- `limitDiagnostics` (number, opcional, default `50`)
Ejemplo:
```json
{
"format": "both",
"saveToFile": true,
"label": "consent-flow",
"includeSteps": true,
"includeDiagnostics": true,
"includeArtifacts": true
}
```
Resultado esperado:
- reporte estructurado para uso del agente
- opcionalmente archivos `.json` y `.md` en `artifacts/`
Comportamiento recomendado:
- por defecto queda guardando a archivo (`saveToFile=true`) para no perder evidencia
- puedes desactivarlo temporalmente en una ejecucion concreta (`saveToFile=false`)
- para cambiar el default de forma persistente usa `browser_config`
---
## Que puede pedirle el usuario a OpenCode
Ejemplos utiles:
- "Abre el browser, entra a `http://localhost:3000` y valida que aparece el boton `Iniciar sesion`."
- "Rellena email y password, envia el formulario y espera una URL que contenga `/dashboard`."
- "Haz scroll hasta el final, toma captura y guardala con etiqueta `dashboard-final`."
- "Ejecuta un `browser_evaluate` para devolver cantidad de filas de la tabla `.orders-table`."
- "Usa `browser_query` para comprobar si `.btn-guardar` esta visible y habilitado."
- "Ejecuta `browser_health` y dime si hay sesion abierta y cuales son los ultimos artifacts."
- "Al terminar la prueba, genera `browser_report` en `both` y guardalo a archivo para revisar resultado y evidencia."
## Limites actuales de esta version
- El diagnostico actual es en memoria de sesion (no persiste historico entre reinicios del proceso).
- Video/trace se activan manualmente por ejecucion; todavia no hay politica automatica "solo en fallo".
- No hay politica de seguridad final para navegacion externa (pendiente de fases posteriores).
- El modo recomendado de v1 es `Chromium managed by Playwright`.
## Errores tipicos y recuperacion
- `Browser session is not open`: ejecuta `browser_open` antes de usar otras tools.
- `Timeout` en `browser_wait`: valida si ya cambio URL/estado con `browser_evaluate`; si ya estas dentro, continua.
- Si la pagina parece bloqueada: ejecuta `browser_observe` para capturar estado/screenshot antes de cortar.
- Si aparece banner de consentimiento: ejecuta `browser_handle_consent` (`reject` o `accept`) y continua.
- Si aparece verificacion humana: ejecuta `browser_handle_human_check`; si no desbloquea, dejar evidencia y hacer handoff manual.
- Selector no encontrado en `browser_click` o `browser_type`: confirma selector con `browser_evaluate` y reintenta con uno mas estable.
- Flujo lento de red: deja que actue la espera nativa; si no alcanza, usa `browser_wait` explicito por selector de estado final.
- Error al evaluar JS en `browser_evaluate`: envia una funcion valida en texto, por ejemplo `(arg) => ({ title: document.title, arg })`.

16
install.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if ! command -v npm >/dev/null 2>&1; then
printf 'npm no esta disponible en el sistema.\n' >&2
exit 1
fi
mkdir -p "$SCRIPT_DIR/artifacts"
npm install --prefix "$SCRIPT_DIR"
npx --yes playwright install chromium
printf 'Instalacion base completada.\n'
printf 'Siguiente paso sugerido: revisar opencode.mcp.example.json\n'

10
opencode.mcp.example.json Normal file
View file

@ -0,0 +1,10 @@
{
"mcpServers": {
"browser-tool": {
"command": "node",
"args": [
"/ruta/a/opencode-browser-tool/dist/server.js"
]
}
}
}

1747
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

25
package.json Normal file
View file

@ -0,0 +1,25 @@
{
"name": "opencode-browser-tool",
"version": "0.1.0",
"private": true,
"description": "Browser MCP tool external to OpenCode",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"check": "bash ./check.sh",
"dev": "tsx src/server.ts",
"start": "node dist/server.js"
},
"engines": {
"node": ">=20"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"playwright": "^1.52.0",
"zod": "^3.24.3"
},
"devDependencies": {
"tsx": "^4.19.3",
"typescript": "^5.8.3"
}
}

View file

@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fixture Diagnostics</title>
</head>
<body>
<h1>Diagnostics Fixture</h1>
<script>
console.log("fixture:console-log")
console.warn("fixture:console-warn")
fetch("/missing-endpoint").catch(() => {})
setTimeout(() => {
throw new Error("fixture:page-error")
}, 50)
</script>
</body>
</html>

View file

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scroll Fixture Long Page</title>
<style>
:root {
color-scheme: light;
}
body {
margin: 0;
font-family: "Courier New", Courier, monospace;
background: linear-gradient(180deg, #f8f9fb 0%, #e9eef7 100%);
}
.topbar {
position: sticky;
top: 0;
z-index: 20;
padding: 10px 14px;
background: #1f2a44;
color: #ffffff;
font-size: 14px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
padding: 16px;
}
.card {
min-height: 220px;
border-radius: 12px;
background: #ffffff;
border: 1px solid #d7deea;
box-shadow: 0 6px 18px rgba(31, 42, 68, 0.12);
padding: 14px;
}
.section {
margin: 24px 16px;
padding: 16px;
border-radius: 12px;
background: #ffffff;
border: 1px solid #d7deea;
}
.scroll-box {
height: 240px;
overflow: auto;
border: 1px solid #9aa7bf;
border-radius: 8px;
padding: 8px;
background: #f4f7fc;
}
.line {
padding: 8px 4px;
border-bottom: 1px dashed #bcc8dc;
}
.tail {
height: 1200px;
margin: 16px;
border-radius: 12px;
background: repeating-linear-gradient(
45deg,
#dce6f6,
#dce6f6 16px,
#edf3fd 16px,
#edf3fd 32px
);
}
</style>
</head>
<body>
<div class="topbar">scrollY: <strong id="scroll-y">0</strong></div>
<div class="grid" id="cards"></div>
<section class="section">
<h2>Scrollable container</h2>
<div class="scroll-box" id="scroll-box"></div>
</section>
<div class="tail"></div>
<script>
const cards = document.getElementById("cards")
for (let i = 1; i <= 18; i += 1) {
const card = document.createElement("article")
card.className = "card"
card.innerHTML = `<h3>Card ${i}</h3><p>Long page fixture content block to force vertical scroll.</p>`
cards.appendChild(card)
}
const box = document.getElementById("scroll-box")
for (let i = 1; i <= 60; i += 1) {
const row = document.createElement("div")
row.className = "line"
row.textContent = `Container row ${i}`
box.appendChild(row)
}
const value = document.getElementById("scroll-y")
const update = () => {
value.textContent = String(Math.round(window.scrollY))
}
update()
window.addEventListener("scroll", update, { passive: true })
</script>
</body>
</html>

View file

@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fixture Select Hover</title>
<style>
body {
font-family: sans-serif;
margin: 24px;
}
#menu {
display: inline-block;
padding: 8px 12px;
border: 1px solid #444;
}
#status {
margin-top: 12px;
color: #0a5;
}
</style>
</head>
<body>
<h1>Browser Tool Fixture</h1>
<div id="menu" onmouseover="window.hovered = true">Hover target</div>
<p id="status"></p>
<label for="kind">Kind:</label>
<select id="kind" onchange="window.kind = this.value">
<option value="">Choose</option>
<option value="general">General</option>
<option value="quirurgico">Quirurgico</option>
<option value="anestesia">Anestesia</option>
</select>
<script>
window.hovered = false
window.kind = ""
</script>
</body>
</html>

View file

@ -0,0 +1,163 @@
import path from "node:path"
import { BrowserManager } from "../dist/browser/manager.js"
const browser = new BrowserManager(path.resolve("./artifacts"))
const query = "puertas cortafuegos de madera"
const targetDomain = "puertastecnicasbcn.com"
const maxPages = 10
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const organicScript = `() => {
const root = document.querySelector("#search") || document
const anchors = Array.from(root.querySelectorAll("a[href]"))
const results = []
for (const a of anchors) {
const href = a.getAttribute("href") || ""
if (!href.startsWith("http")) continue
if (href.includes("google.") || href.includes("webcache.googleusercontent.com")) continue
const h3 = a.querySelector("h3")
if (!h3) continue
const title = (h3.textContent || "").trim()
if (!title) continue
if (!results.some((x) => x.href === href)) {
results.push({ href, title })
}
}
return results
}`
const run = async () => {
const out = { pages: [] }
out.open = await browser.open({
headless: false,
width: 1366,
height: 900,
browserKind: "testing",
persistentProfile: true,
userDataDir: "/home/pancho/.chromium-perfil-google",
startUrl: "https://www.google.es",
recordVideo: false,
})
out.cookies = await browser.handleConsent("accept").catch((e) => ({ error: e.message }))
await browser.type('textarea[name="q"]', query)
await browser.press("Enter")
await browser.waitFor({ for: "timeout", value: "1200" })
out.afterSearch = await browser.observe().catch((e) => ({ error: e.message }))
if ((out.afterSearch.url || "").includes("/sorry/index")) {
out.humanAttempt = await browser.handleHumanCheck().catch((e) => ({ error: e.message }))
await browser.waitFor({ for: "timeout", value: "3000" })
out.afterChallenge = await browser.observe().catch((e) => ({ error: e.message }))
}
let found = null
let globalRank = 0
for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) {
await browser.waitFor({ for: "timeout", value: "800" })
const currentUrl = browser.getState().currentUrl || ""
if (!currentUrl.includes("/search")) {
out.pages.push({ page: pageNo, status: "not_on_search", url: currentUrl })
break
}
const extracted = await browser.evaluate(organicScript, {}).catch(() => ({ result: [] }))
const organic = Array.isArray(extracted.result) ? extracted.result : []
const idx = organic.findIndex((x) => {
try {
return new URL(String(x.href)).hostname.includes(targetDomain)
} catch {
return String(x.href).includes(targetDomain)
}
})
out.pages.push({
page: pageNo,
url: currentUrl,
organicCount: organic.length,
found: idx >= 0,
positionOnPage: idx >= 0 ? idx + 1 : null,
})
if (idx >= 0) {
globalRank += idx + 1
found = {
page: pageNo,
positionOnPage: idx + 1,
globalRank,
href: organic[idx].href,
title: organic[idx].title,
}
break
}
globalRank += organic.length
let moved = false
for (const sel of ["#pnnext", "a#pnnext", "a[aria-label*='siguiente' i]", "a[aria-label*='next' i]"]) {
const ex = await browser.query({ selector: sel, mode: "exists" }).catch(() => ({ result: false }))
if (ex.result === true) {
await browser.click(sel).catch(() => null)
moved = true
break
}
}
if (!moved) {
const tried = await browser
.evaluate(
`() => {
const nodes = Array.from(document.querySelectorAll("a,button,[role=button]"))
for (const n of nodes) {
const t = (n.innerText || "").trim()
if (!t) continue
if (t === "Siguiente" || t.includes("Siguiente") || t === "Next" || t.includes("Next")) {
n.click()
return { clicked: true, text: t }
}
}
return { clicked: false, text: null }
}`,
{},
)
.catch(() => ({ result: { clicked: false, text: null } }))
out.pages[out.pages.length - 1].nextFallback = tried.result
if (!tried.result?.clicked) {
break
}
}
await sleep(1400)
}
out.result = found || { found: false }
if (found?.href) {
await browser.navigate(found.href, "domcontentloaded")
await browser.waitFor({ for: "timeout", value: "1000" })
out.destinationObserve = await browser.observe().catch((e) => ({ error: e.message }))
out.destinationSnapshot = await browser.snapshot({ label: "google-puertas-destination", fullPage: false })
}
out.finalObserve = await browser.observe().catch((e) => ({ error: e.message }))
out.close = await browser.close()
console.log(JSON.stringify(out, null, 2))
}
run().catch(async (err) => {
console.error("GOOGLE_PUERTAS_ERROR", err?.message || err)
try {
const snap = await browser.snapshot({ label: "google-puertas-error", fullPage: true })
console.error("ERROR_SNAPSHOT", snap.filePath)
} catch {
// ignore
}
try {
await browser.close()
} catch {
// ignore
}
process.exit(1)
})

246
scripts/google_es_v8d.mjs Normal file
View file

@ -0,0 +1,246 @@
import path from "node:path"
import { BrowserManager } from "../dist/browser/manager.js"
const browser = new BrowserManager(path.resolve("./artifacts"))
const targetDomain = "barranquismogranada.com"
const maxPages = 10
const out = { pages: [], debug: [] }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const organicScript = `() => {
const root = document.querySelector("#search") || document
const anchors = Array.from(root.querySelectorAll("a[href]"))
const results = []
for (const a of anchors) {
const href = a.getAttribute("href") || ""
if (!href.startsWith("http")) continue
if (href.includes("google.") || href.includes("webcache.googleusercontent.com")) continue
const h3 = a.querySelector("h3")
if (!h3) continue
const title = (h3.textContent || "").trim()
if (!title) continue
if (!results.some((x) => x.href === href)) results.push({ href, title })
}
return results
}`
const nextFallbackScript = `() => {
const nodes = Array.from(document.querySelectorAll("a,button,[role=button]"))
for (const n of nodes) {
const t = (n.innerText || "").trim()
if (!t) continue
if (t === "Siguiente" || t.includes("Siguiente") || t === "Next" || t.includes("Next")) {
n.click()
return { clicked: true, text: t }
}
}
return { clicked: false, text: null }
}`
const safeEval = async (script, arg, tag) => {
for (let i = 0; i < 12; i += 1) {
try {
return await browser.evaluate(script, arg)
} catch {
out.debug.push(`${tag}:eval-retry-${i + 1}`)
await sleep(450)
}
}
return { result: null }
}
const safeObserve = async (tag) => {
for (let i = 0; i < 8; i += 1) {
try {
return await browser.observe()
} catch {
out.debug.push(`${tag}:observe-retry-${i + 1}`)
await sleep(450)
}
}
return { url: browser.getState().currentUrl, failed: true }
}
const safeScroll = async (y) => {
try {
return await browser.scroll({ y })
} catch {
return { movedY: 0 }
}
}
const run = async () => {
out.open = await browser.open({
headless: false,
width: 1366,
height: 900,
browserKind: "testing",
persistentProfile: true,
userDataDir: "/home/pancho/.chromium-perfil-google",
startUrl: "https://www.google.es",
recordVideo: false,
recordLabel: "google-es-organic-click-scroll-v8d",
})
out.cookies = await browser.handleConsent("accept").catch((e) => ({ error: e.message }))
await browser.type('textarea[name="q"]', "barranquismo granada")
await browser.press("Enter")
await browser.waitFor({ for: "timeout", value: "1200" })
out.afterSearch = await safeObserve("afterSearch")
if ((out.afterSearch.url || "").includes("/sorry/index")) {
out.humanAttempt = await browser.handleHumanCheck().catch((e) => ({ error: e.message }))
out.before3s = await browser.snapshot({ label: "google-v8d-before-3s", fullPage: true }).catch((e) => ({
error: e.message,
}))
await browser.waitFor({ for: "timeout", value: "3000" })
out.after3sObserve = await safeObserve("after3s")
out.after3s = await browser.snapshot({ label: "google-v8d-after-3s", fullPage: true }).catch((e) => ({
error: e.message,
}))
}
let found = null
let globalRank = 0
for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) {
await browser.waitFor({ for: "timeout", value: "700" })
const currentUrl = browser.getState().currentUrl || ""
if (!currentUrl.includes("/search")) {
out.pages.push({ page: pageNo, status: "not_on_search", url: currentUrl })
break
}
const extracted = await safeEval(organicScript, {}, `organic-p${pageNo}`)
const organic = Array.isArray(extracted.result) ? extracted.result : []
const idx = organic.findIndex((x) => {
try {
return new URL(String(x.href)).hostname.includes(targetDomain)
} catch {
return String(x.href).includes(targetDomain)
}
})
out.pages.push({
page: pageNo,
url: currentUrl,
organicCount: organic.length,
found: idx >= 0,
positionOnPage: idx >= 0 ? idx + 1 : null,
})
if (idx >= 0) {
globalRank += idx + 1
found = {
page: pageNo,
positionOnPage: idx + 1,
globalRank,
href: organic[idx].href,
title: organic[idx].title,
}
break
}
globalRank += organic.length
await safeScroll(680)
await sleep(180)
await safeScroll(-240)
let moved = false
for (const sel of ["#pnnext", "a#pnnext", "a[aria-label*='siguiente' i]", "a[aria-label*='next' i]"]) {
const ex = await browser.query({ selector: sel, mode: "exists" }).catch(() => ({ result: false }))
if (ex.result === true) {
await browser.click(sel).catch(() => null)
moved = true
break
}
}
if (!moved) {
const tried = await safeEval(nextFallbackScript, {}, `next-p${pageNo}`)
out.pages[out.pages.length - 1].nextFallback = tried.result
if (!tried.result?.clicked) break
}
await sleep(1200)
}
out.result = found || { found: false }
if (found?.href) {
let clicked = false
const byHrefSelector = `a[href="${String(found.href).replace(/"/g, '\\"')}"]`
const exists = await browser.query({ selector: byHrefSelector, mode: "exists" }).catch(() => ({ result: false }))
if (exists.result === true) {
await browser
.click(byHrefSelector)
.then(() => {
clicked = true
})
.catch(() => null)
}
if (!clicked) {
const clickRes = await safeEval(
`(arg) => {
const links = Array.from(document.querySelectorAll("#search a[href], a[href]"))
const t = links.find((a) => (a.getAttribute("href") || "") === arg.href)
if (!t) return { clicked: false }
t.click()
return { clicked: true }
}`,
{ href: found.href },
"click-target",
)
clicked = Boolean(clickRes.result?.clicked)
}
out.clickTarget = { clicked }
if (!clicked) {
await browser.navigate(found.href, "domcontentloaded")
out.clickFallbackNavigate = true
}
await browser.waitFor({ for: "timeout", value: "1000" })
out.destinationObserve = await safeObserve("destination")
let loops = 0
let lastMoved = 1
while (loops < 50 && lastMoved !== 0) {
const s = await safeScroll(420)
lastMoved = Number(s.movedY ?? 0)
await sleep(220)
loops += 1
}
out.scrollRun = { loops, lastMoved }
out.destinationSnapshot = await browser.snapshot({ label: "google-v8d-destination-bottom", fullPage: true }).catch((e) => ({
error: e.message,
}))
}
out.finalObserve = await safeObserve("final")
out.finalSnapshot = await browser.snapshot({ label: "google-v8d-final", fullPage: true }).catch((e) => ({
error: e.message,
}))
out.close = await browser.close()
console.log(JSON.stringify(out, null, 2))
}
run().catch(async (err) => {
console.error("GOOGLE_V8D_ERROR", err?.stack || err?.message || err)
try {
const snap = await browser.snapshot({ label: "google-v8d-error", fullPage: true })
console.error("ERROR_SNAPSHOT", snap.filePath)
} catch {
// ignore
}
try {
await browser.close()
} catch {
// ignore
}
process.exit(1)
})

View file

@ -0,0 +1,113 @@
import path from "node:path"
import { BrowserManager } from "../dist/browser/manager.js"
const browser = new BrowserManager(path.resolve("./artifacts"))
const maxPages = 8
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const linksScript = `() =>
Array.from(document.querySelectorAll('a[href]'))
.map((a) => a.getAttribute('href') || '')
.filter(Boolean)`
const nextScript = `() => {
const labels = ['Siguiente', 'Next']
const nodes = Array.from(document.querySelectorAll('a,button,[role="button"]'))
for (const n of nodes) {
const t = (n.innerText || '').trim()
if (!t) continue
if (labels.some((k) => t === k || t.includes(k))) {
n.click()
return { clicked: true, text: t }
}
}
const byId = document.querySelector('#pnnext')
if (byId) {
byId.click()
return { clicked: true, text: '#pnnext' }
}
return { clicked: false, text: null }
}`
const run = async () => {
const out = { pages: [] }
out.open = await browser.open({
headless: false,
width: 1366,
height: 900,
startUrl: "https://www.google.com",
recordVideo: true,
recordLabel: "google-pagination-v4",
})
out.consent = await browser.handleConsent("reject").catch((e) => ({ error: e.message }))
await browser.type('textarea[name="q"]', "barranquismo granada")
await browser.press("Enter")
out.observeStart = await browser.observe()
if ((out.observeStart.url || "").includes("/sorry/index")) {
out.human = await browser.handleHumanCheck().catch((e) => ({ error: e.message }))
await browser.waitFor({ for: "timeout", value: "4000" })
out.observeAfterHuman = await browser.observe().catch((e) => ({ error: e.message }))
}
let found = null
for (let p = 1; p <= maxPages; p += 1) {
const url = browser.getState().currentUrl || ""
if (!url.includes("/search")) {
out.pages.push({ page: p, status: "not-on-search", url })
break
}
const links = await browser.evaluate(linksScript, undefined)
const normalized = Array.isArray(links.result) ? links.result.map(String) : []
const matchIndex = normalized.findIndex((href) => href.includes("barranquismogranada.com"))
out.pages.push({
page: p,
url,
links: normalized.length,
found: matchIndex >= 0,
idx: matchIndex >= 0 ? matchIndex + 1 : null,
})
if (matchIndex >= 0) {
found = { page: p, positionOnPage: matchIndex + 1, url }
break
}
const nextTried = await browser.evaluate(nextScript, undefined)
out.pages[out.pages.length - 1].next = nextTried.result
if (!nextTried.result?.clicked) {
break
}
await wait(1400)
}
out.result = found || { found: false }
out.finalObserve = await browser.observe().catch((e) => ({ error: e.message }))
out.snapshot = await browser.snapshot({ label: "google-pagination-v4-final", fullPage: true }).catch((e) => ({
error: e.message,
}))
out.close = await browser.close()
console.log(JSON.stringify(out, null, 2))
}
run().catch(async (err) => {
console.error("GOOGLE_PAGINATION_V4_ERROR", err?.message || err)
try {
const snap = await browser.snapshot({ label: "google-pagination-v4-error", fullPage: true })
console.error("ERROR_SNAPSHOT", snap.filePath)
} catch {
// ignore
}
try {
await browser.close()
} catch {
// ignore
}
process.exit(1)
})

1089
src/browser/manager.ts Normal file

File diff suppressed because it is too large Load diff

1049
src/server.ts Normal file

File diff suppressed because it is too large Load diff

15
tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"]
}