From 420c6c85bb788252306c92cd0f8f5a470d528c1d Mon Sep 17 00:00:00 2001 From: Paco Date: Fri, 24 Apr 2026 23:59:41 +0200 Subject: [PATCH] chore: initialize browser tool devlog from project root --- .gitignore | 6 + README.md | 101 + check.sh | 33 + config/browser-tool.config.json | 16 + docs/PLAN_CIERRE_INSTALABLE.md | 59 + docs/PLAN_DE_DESARROLLO.md | 368 ++++ docs/QUICKSTART.md | 152 ++ docs/REGISTRO_SITUACIONES.md | 218 ++ docs/TODO.md | 114 ++ docs/VALIDACION_ENTORNO_LIMPIO.md | 93 + .../CONTEXTO_ACTIVO_BROWSER.md | 68 + docs/contexto_workspace/HISTORIAL_SESIONES.md | 1187 +++++++++++ .../SKILL_CONTINUIDAD_BROWSER.md | 28 + ...ea_y_funcionamiento_herramienta_browser.md | 120 ++ docs/manual_de_uso_heramienta.md | 686 +++++++ install.sh | 16 + opencode.mcp.example.json | 10 + package-lock.json | 1747 +++++++++++++++++ package.json | 25 + scripts/fixture_diagnostics.html | 19 + scripts/fixture_scroll_long.html | 109 + scripts/fixture_select_hover.html | 41 + scripts/google_es_puertas_rank.mjs | 163 ++ scripts/google_es_v8d.mjs | 246 +++ scripts/google_pagination_v4.mjs | 113 ++ src/browser/manager.ts | 1089 ++++++++++ src/server.ts | 1049 ++++++++++ tsconfig.json | 15 + 28 files changed, 7891 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 check.sh create mode 100644 config/browser-tool.config.json create mode 100644 docs/PLAN_CIERRE_INSTALABLE.md create mode 100644 docs/PLAN_DE_DESARROLLO.md create mode 100644 docs/QUICKSTART.md create mode 100644 docs/REGISTRO_SITUACIONES.md create mode 100644 docs/TODO.md create mode 100644 docs/VALIDACION_ENTORNO_LIMPIO.md create mode 100644 docs/contexto_workspace/CONTEXTO_ACTIVO_BROWSER.md create mode 100644 docs/contexto_workspace/HISTORIAL_SESIONES.md create mode 100644 docs/contexto_workspace/SKILL_CONTINUIDAD_BROWSER.md create mode 100644 docs/contexto_workspace/idea_y_funcionamiento_herramienta_browser.md create mode 100644 docs/manual_de_uso_heramienta.md create mode 100755 install.sh create mode 100644 opencode.mcp.example.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/fixture_diagnostics.html create mode 100644 scripts/fixture_scroll_long.html create mode 100644 scripts/fixture_select_hover.html create mode 100644 scripts/google_es_puertas_rank.mjs create mode 100644 scripts/google_es_v8d.mjs create mode 100644 scripts/google_pagination_v4.mjs create mode 100644 src/browser/manager.ts create mode 100644 src/server.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e31850 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +artifacts/ +playwright-report/ +test-results/ +.opencode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..31ca489 --- /dev/null +++ b/README.md @@ -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. diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..176ca21 --- /dev/null +++ b/check.sh @@ -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 diff --git a/config/browser-tool.config.json b/config/browser-tool.config.json new file mode 100644 index 0000000..5031f73 --- /dev/null +++ b/config/browser-tool.config.json @@ -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" + } +} diff --git a/docs/PLAN_CIERRE_INSTALABLE.md b/docs/PLAN_CIERRE_INSTALABLE.md new file mode 100644 index 0000000..0975b9f --- /dev/null +++ b/docs/PLAN_CIERRE_INSTALABLE.md @@ -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). diff --git a/docs/PLAN_DE_DESARROLLO.md b/docs/PLAN_DE_DESARROLLO.md new file mode 100644 index 0000000..883378a --- /dev/null +++ b/docs/PLAN_DE_DESARROLLO.md @@ -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 diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md new file mode 100644 index 0000000..9c79fa1 --- /dev/null +++ b/docs/QUICKSTART.md @@ -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 +} +``` diff --git a/docs/REGISTRO_SITUACIONES.md b/docs/REGISTRO_SITUACIONES.md new file mode 100644 index 0000000..51d6613 --- /dev/null +++ b/docs/REGISTRO_SITUACIONES.md @@ -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. diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..f1ef019 --- /dev/null +++ b/docs/TODO.md @@ -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` diff --git a/docs/VALIDACION_ENTORNO_LIMPIO.md b/docs/VALIDACION_ENTORNO_LIMPIO.md new file mode 100644 index 0000000..c4ce280 --- /dev/null +++ b/docs/VALIDACION_ENTORNO_LIMPIO.md @@ -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: diff --git a/docs/contexto_workspace/CONTEXTO_ACTIVO_BROWSER.md b/docs/contexto_workspace/CONTEXTO_ACTIVO_BROWSER.md new file mode 100644 index 0000000..3904f75 --- /dev/null +++ b/docs/contexto_workspace/CONTEXTO_ACTIVO_BROWSER.md @@ -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. diff --git a/docs/contexto_workspace/HISTORIAL_SESIONES.md b/docs/contexto_workspace/HISTORIAL_SESIONES.md new file mode 100644 index 0000000..8befc70 --- /dev/null +++ b/docs/contexto_workspace/HISTORIAL_SESIONES.md @@ -0,0 +1,1187 @@ +# Historial de sesiones + +## Proyecto: Transformar OpenCode de un agente con contexto de sesion en un sistema con memoria estructurada, identidad robusta y prompt dinamico contextual + +Este archivo mantiene un registro de agentes, sesiones persistentes e interacciones relevantes realizadas en este proyecto. + +En este contexto, el termino `sesion` debe entenderse principalmente como la sesion persistente de OpenCode identificada por su `session_id`. Cada una de esas sesiones queda asociada a un agente concreto para poder saber con claridad quien hizo que cosa dentro del mismo workspace compartido. + +Lo que en la practica se va acumulando dentro de ese historial no son solo sesiones cerradas como bloques monoliticos, sino tambien `interacciones` ocurridas dentro de una misma sesion a lo largo del tiempo. Una interaccion puede extenderse durante minutos u horas y contener multiples acciones, decisiones, cambios de rumbo, hallazgos, modificaciones o conclusiones parciales. + +Por eso, este documento no debe usarse solo como un cierre final de cada sesion, sino como un registro progresivo de lo relevante que va ocurriendo dentro de ella. Si en una interaccion se realizan muchas acciones distintas, todas ellas deben poder quedar reflejadas aqui aunque pertenezcan a la misma sesion persistente. + +Objetivo practico de este documento: + +- registrar el nombre identificable de cada agente +- asociar cada agente a su `session_id` +- distinguir agentes distintos dentro del mismo workspace +- registrar interacciones relevantes dentro de una misma sesion +- dejar trazabilidad suficiente para reconstruir decisiones, cambios y autoria + +El objetivo del proyecto es: +- no quieres solo “usar un asistente”, quieres comprender su arquitectura para poder rediseñarla o reforzarla de forma seria +- estas construyendo una base documental para que cualquier modelo futuro pueda retomar el trabajo con continuidad y criterio +- buscas un OpenCode mas inteligente a nivel estructural: con mejor memoria, mejor identidad de agentes y mejor adaptacion contextual +- te interesa que el agente se acerque mas a una forma de razonamiento humano: recuperar solo lo relevante, discriminar fino y actuar segun contexto, no repetir siempre el mismo bloque estatico +- ahora mismo estas en fase de analisis e investigacion, no de implementacion; quieres entender bien el terreno antes de decidir diseno, coste y esfuerzo tecnico + +--- + +## Como identificar a un agente + +### Si no sabes que agente es este agente o esta interaccion + +Pregunta: +> "¿Quién eres?" o "Identifícate" + +El agente debe: +1. Leer este archivo (`HISTORIAL_SESIONES.md`) +2. Obtener su `session_id` de OpenCode siguiendo `docs/sesion_actual_opencode.md` +3. Buscar su `session_id` en la tabla de "Indice de Agentes" de abajo +4. Responder con su nombre, proyecto y responsabilidad + +Ejemplo de respuesta esperada: +``` +Soy el Agente Backend. +Trabajo en el proyecto: [Nombre del Proyecto] +Mi OpenCode session_id es: [session_id] +Me encargo de: [Responsabilidad principal] +``` + +--- + +## Indice de agentes + +| Agente | Responsabilidad | OpenCode session_id | +|--------|-----------------|--------------------| +| **Agente VSC** | Explorar proyectos en VSC para identificar como aprovechar OpenCode; apoyar analisis, cambios y agregado de codigo cuando el usuario lo solicite. | `ses_3355059f1ffe13etDo5AY3pDfc` | +| **ModulosIA** | Analizar y potenciar modulos, skills, codigo y piezas concretas que ayuden a potenciar OpenCode y, a futuro, una version propia centrada en modulos o partes especificas. | `ses_3180b7b07ffeqiUzgxsMksatt5` | +| **Subagente OpenCode** | Apoyo de analisis e investigacion del funcionamiento interno de OpenCode. | `ses_337c66f9fffeFSn3rw04bwb8Ct` | +| **Agente Auditor ClaudeCode** | Auditar el codigo de ClaudeCode para detectar ideas, patrones y piezas interesantes que puedan integrarse en nuestra propia herramienta a partir de la publicacion abierta de Anthropic. | `ses_2b54f59efffeegXeimLGxWdfXL` | +| **Agente Hermes Ayuda** | Temas relacionados con Agent-Hermes, instalacion, investigacion y todo lo que se necesite relacionado con Agent-Hermes. | `ses_296a5859bffeXabw9ih9fR3MZ2` | +| **Agente modulo Browser Opencode** | Responsable tecnico del proyecto para disenar y crear un browser que permita a OpenCode navegar, interactuar y probar aplicaciones del workspace con un comportamiento similar a Antigravity. | `ses_248d8d7b9ffeXSm1L7BDQsWLOw` | + +--- + +## Registro de interacciones + +### Sesion 1 (2026-03-08) - Agente VSC +**Agente:** **Agente VSC** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_3355059f1ffe13etDo5AY3pDfc` +**Referencia de sesion:** `sesion-1` + +#### Trabajo realizado +- ✅ Leido `docs/README.md` +- ✅ Leido `docs/INDICE_DOCUMENTACION.md` +- ✅ Leido `docs/HISTORIAL_SESIONES.md` +- ✅ Registrada la asignacion del agente `Agente VSC` + +#### Estado final +- Agente asignado para exploracion de proyectos en VSC y apoyo con uso de OpenCode. +- Nombre del proyecto actualizado a `Transformar OpenCode de un agente con contexto de sesion en un sistema con memoria estructurada, identidad robusta y prompt dinamico contextual`. +- Objetivo actualizado para reflejar investigacion, memoria estructurada, identidad de agentes y prompt dinamico contextual. +- `OpenCode session_id` recuperada y confirmada como `ses_3355059f1ffe13etDo5AY3pDfc` siguiendo `docs/sesion_actual_opencode.md`. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` + +### Sesion 2 (2026-03-09) - Agente VSC +**Agente:** **Agente VSC** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `no recuperada en historial; documento firmado por Agente VSC` +**Referencia de sesion:** `auditoria-opencode-oficial-2026-03-09` + +#### Trabajo realizado +- ✅ Ejecutada una auditoria inicial del repositorio oficial clonado en `opencode-oficial/` +- ✅ Mapeadas las carpetas y paquetes principales del monorepo +- ✅ Identificados componentes clave de `packages/opencode`, `packages/sdk/js`, `packages/app`, `packages/ui`, `packages/web` y `packages/plugin` +- ✅ Documentados hallazgos sobre server local, persistencia SQLite, agentes built-in, system prompt por capas, providers y extensibilidad +- ✅ Registradas zonas candidatas para futuras investigaciones especificas en `session/`, `agent/`, `provider/`, `tool/`, `config/` y `server/routes/` + +#### Estado final +- Quedo creada la base documental de auditoria tecnica del codigo fuente oficial de OpenCode. +- La autoria queda respaldada por `docs/AUDITORIA_OPENCODE_OFICIAL.md`, firmado como `Agente VSC` en fecha `2026-03-09`. +- La `session_id` original de esa auditoria no quedo registrada en el historial y no ha podido recuperarse desde la base del workspace actual. + +#### Archivos modificados +- `docs/AUDITORIA_OPENCODE_OFICIAL.md` + +### Sesion 3 (2026-04-01) - ModulosIA +**Agente:** **ModulosIA** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_3180b7b07ffeqiUzgxsMksatt5` +**Referencia de sesion:** `interaccion-documentacion-y-modulo-gpt-plus-browser-2026-04-01` + +#### Trabajo realizado +- ✅ Investigado de forma focalizada como OpenCode conecta `ChatGPT Plus/Pro` mediante navegador +- ✅ Identificados y documentados los ficheros clave del flujo `OAuth/PKCE` en el repo `opencode-oficial/` +- ✅ Creado el pendiente del modulo reutilizable de conexion con `GPT Plus/Pro` via browser +- ✅ Creada la carpeta del subproyecto `gpt-plus-browser-module/` con su `README.md` inicial +- ✅ Creado `docs/MODULO_GPT_PLUS_BROWSER.md` con hallazgos, alcance, tecnologia recomendada y plan por fases +- ✅ Creado `docs/skill.md` y analizado su contenido +- ✅ Recuperado el `session_id` real del workspace usando `docs/sesion_actual_opencode.md` +- ✅ Registrado el agente `ModulosIA` y asociada esta interaccion a su `OpenCode session_id` +- ✅ Registrada retrospectivamente la auditoria global del codigo de OpenCode en este historial +- ✅ Refinada la terminologia y las reglas de uso de `docs/HISTORIAL_SESIONES.md` para distinguir `session_id`, sesion persistente e interacciones +- ✅ Indexado `docs/sesion_actual_opencode.md` en `docs/INDICE_DOCUMENTACION.md` con instrucciones explicitas de uso para agentes + +#### Estado final +- Quedo documentado el subproyecto del modulo GPT Plus browser y su plan inicial. +- El historial de sesiones quedo mas preciso respecto a agentes, `session_id` e interacciones. +- El indice documental ya incluye el documento canonico para extraer la sesion actual del workspace. +- Quedo corregida la autoria de esta interaccion para reflejar a `ModulosIA` en lugar de `Agente VSC`. + +#### Archivos modificados +- `docs/PENDIENTES.md` +- `docs/MODULO_GPT_PLUS_BROWSER.md` +- `docs/INDICE_DOCUMENTACION.md` +- `docs/HISTORIAL_SESIONES.md` +- `docs/skill.md` +- `gpt-plus-browser-module/README.md` + +### Sesion 4 (2026-04-01) - Agente VSC +**Agente:** **Agente VSC** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_3355059f1ffe13etDo5AY3pDfc` +**Referencia de sesion:** `recuperacion-session-id-agente-vsc-2026-04-01` + +#### Trabajo realizado +- ✅ Leido `docs/HISTORIAL_SESIONES.md` +- ✅ Leido `docs/sesion_actual_opencode.md` +- ✅ Ejecutado el comando canonico de OpenCode para identificar la sesion actual del workspace +- ✅ Confirmado el `session_id` real del agente `Agente VSC` +- ✅ Corregido el indice de agentes para asociar `Agente VSC` con su `session_id` real + +#### Estado final +- `Agente VSC` quedo asociado correctamente a la sesion persistente `ses_3355059f1ffe13etDo5AY3pDfc`. +- Queda resuelta la ambiguedad entre nombre de agente y sesion tecnica de OpenCode para este workspace. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` + +### Sesion 5 (2026-04-01) - Subagente OpenCode +**Agente:** **Subagente OpenCode** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_337c66f9fffeFSn3rw04bwb8Ct` +**Referencia de sesion:** `analisis-funcionamiento-opencode-y-preparacion-documental-2026-04-01` + +#### Trabajo realizado +- ✅ Analizado el funcionamiento practico de OpenCode a nivel de sesiones, workspace, proyecto y persistencia local +- ✅ Investigados el proveedor activo, el modelo en uso, el metodo de autenticacion y la ruta tecnica de conexion con OpenAI +- ✅ Revisados contexto, limite de tokens, compaction automatica, LSP y comportamiento del panel lateral +- ✅ Comprobadas rutas locales relevantes de OpenCode, incluyendo configuracion, base de datos, logs y datos de autenticacion +- ✅ Investigado el stack tecnico de OpenCode tanto en la instalacion local como en el repositorio oficial +- ✅ Aclarada la diferencia entre memoria de sesion, configuracion global e instrucciones persistentes del proyecto +- ✅ Solucionado problema de espacio en disco mediante desinstalacion de Docker y limpieza de cache APT (~2GB recuperados) +- ✅ Realizada comparativa entre OpenCode, Claude Code, Agent Zero y Hermes Agent para identificar oportunidades de mejora +- ✅ Diseñado e iniciado el proyecto `memoria_persistente_opencode` para dotar a OpenCode de memoria estructurada y auditable +- ✅ Creados `docs/USER_PROFILE.md` y `docs/LEARNED_SKILLS.md` como pilares de la memoria de workspace +- ✅ Creado `memoria_persistente_opencode/PLAN_INICIAL.md` con los objetivos y la base filosofica de la Desconfianza Estructural +- ✅ Creado `docs/guia-configuracion-agentes.md` para fijar la informacion sobre aprendizaje, configuraciones y reutilizacion de conocimiento +- ✅ Limpiados y adaptados `docs/README.md`, `docs/INDICE_DOCUMENTACION.md` y `docs/HISTORIAL_SESIONES.md` para reutilizar su estructura en este proyecto +- ✅ Localizada en el historial la auditoria previa del codigo fuente oficial de OpenCode realizada sobre `opencode-oficial/` +- ✅ Abierto `VS Code` sobre este workspace para apoyar un flujo de trabajo combinado editor + OpenCode +- ✅ Indexados todos los nuevos proyectos y documentos en `docs/PENDIENTES.md` y `docs/INDICE_DOCUMENTACION.md` + +#### Estado final +- Quedo registrado el agente `Subagente OpenCode` con la `session_id` `ses_337c66f9fffeFSn3rw04bwb8Ct`. +- Quedo resumida esta sesion como fase de analisis operativo, investigacion del funcionamiento interno de OpenCode, limpieza del sistema y preparacion documental del workspace. +- Se ha iniciado formalmente el proyecto de Memoria Persistente bajo el paradigma de Desconfianza Estructural. +- Quedo identificada la referencia documental de la auditoria previa del codigo fuente oficial de OpenCode en este mismo proyecto. + +#### Archivos modificados +- `docs/guia-configuracion-agentes.md` +- `docs/README.md` +- `docs/INDICE_DOCUMENTACION.md` +- `docs/HISTORIAL_SESIONES.md` +- `docs/PENDIENTES.md` +- `docs/USER_PROFILE.md` +- `docs/LEARNED_SKILLS.md` +- `memoria_persistente_opencode/PLAN_INICIAL.md` + +### Sesion 6 (2026-04-01) - Agente Auditor ClaudeCode +**Agente:** **Agente Auditor ClaudeCode** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_2b54f59efffeegXeimLGxWdfXL` +**Referencia de sesion:** `auditoria-claudecode-registro-inicial-2026-04-01` + +#### Trabajo realizado +- ✅ Leido `docs/README.md` +- ✅ Leido `docs/INDICE_DOCUMENTACION.md` +- ✅ Leido `docs/HISTORIAL_SESIONES.md` +- ✅ Leido `docs/sesion_actual_opencode.md` +- ✅ Recuperada la `OpenCode session_id` real del workspace actual +- ✅ Registrado el agente `Agente Auditor ClaudeCode` +- ✅ Definida su responsabilidad de auditar ClaudeCode para encontrar integraciones utiles para nuestra propia herramienta +- ✅ Ejecutada una primera pasada de auditoria sobre `claudecode/map_con_el_codigo_claude_code_2.1.88/src/` +- ✅ Identificados hallazgos iniciales sobre `QueryEngine`, prompt por capas, memoria, agentes, permisos, persistencia y extensibilidad +- ✅ Creado `docs/auditoria_claudecde.md` con el resumen literal de la primera pasada +- ✅ Creado `docs/pendiente_claudecode.md` con las zonas recomendadas para auditoria profunda +- ✅ Indexados ambos documentos en `docs/INDICE_DOCUMENTACION.md` + +#### Estado final +- Quedo dado de alta `Agente Auditor ClaudeCode` en el indice de agentes. +- La sesion persistente `ses_2b54f59efffeegXeimLGxWdfXL` queda asociada a la linea de trabajo de auditoria de ClaudeCode. +- Quedo documentada la primera pasada de auditoria y definido el siguiente bloque de analisis profundo. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `docs/INDICE_DOCUMENTACION.md` +- `docs/auditoria_claudecde.md` +- `docs/pendiente_claudecode.md` + +### Sesion 7 (2026-04-07) - Agente Hermes Ayuda +**Agente:** **Agente Hermes Ayuda** +**Modelo:** nemotron +**OpenCode session_id:** `ses_296a5859bffeXabw9ih9fR3MZ2` +**Referencia de sesion:** `hermes-agent-instalacion-investigacion-guia` + +#### Trabajo realizado +- ✅ Leido `docs/HISTORIAL_SESIONES.md` +- ✅ Obtuvo su `session_id` de OpenCode siguiendo `docs/sesion_actual_opencode.md` +- ✅ Registrado como Agente Hermes Ayuda con rol de soporte sobre Agent-Hermes: instalacion, investigacion y guia +- ✅ Se registro en el indice de agentes con el nombre y rol correctos + +#### Estado final +- Agente Hermes Ayuda registrado correctamente con su nombre, proyecto y responsabilidad +- OpenCode session_id confirmada como `ses_296a5859bffeXabw9ih9fR3MZ2` +- Listo para asistir en todo lo relacionado con Agent-Hermes + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` + +### Sesion 8 (2026-04-22) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `registro-inicial-browser-opencode-2026-04-22` + +#### Trabajo realizado +- ✅ Leido `docs/README.md` +- ✅ Leido `docs/HISTORIAL_SESIONES.md` +- ✅ Leido `docs/sesion_actual_opencode.md` +- ✅ Recuperada la `OpenCode session_id` real del workspace mediante el comando canonico +- ✅ Registrado el agente `Agente modulo Browser Opencode` en el indice de agentes +- ✅ Definido su rol como responsable tecnico del proyecto Browser para OpenCode +- ✅ Registrado el alcance inicial del proyecto: disenar y crear un browser para que OpenCode pueda navegar, interactuar y probar apps del workspace de forma similar a Antigravity + +#### Estado final +- Agente modulo Browser Opencode registrado correctamente con su nombre, proyecto y responsabilidad tecnica. +- OpenCode `session_id` confirmada como `ses_248d8d7b9ffeXSm1L7BDQsWLOw`. +- Queda formalmente abierta la linea de trabajo del browser de pruebas para OpenCode. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` + +### Sesion 9 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `plan-desarrollo-y-todo-opencode-browser-tool-2026-04-23` + +#### Trabajo realizado +- ✅ Continuada la conversacion tecnica para aterrizar el alcance de la herramienta browser externa para OpenCode +- ✅ Confirmado el criterio estructural principal: solucion externa a OpenCode, desacoplada de su core y resistente a futuras actualizaciones +- ✅ Confirmada la direccion tecnica inicial: `MCP` como integracion con OpenCode, `Playwright + Chromium` como base de la v1 y `CDP` como extension progresiva +- ✅ Definido que la v1 debe priorizar la interaccion efectiva entre OpenCode y el browser antes de abordar politicas de uso avanzado sobre webs externas +- ✅ Creada la carpeta del proyecto `opencode-browser-tool/` para aislar todo el desarrollo fuera de la raiz del workspace +- ✅ Creados los documentos `opencode-browser-tool/docs/PLAN_DE_DESARROLLO.md` y `opencode-browser-tool/docs/TODO.md` +- ✅ Documentado el plan de desarrollo con arquitectura por fases, alcance de v1, continuidad hacia v2 e instalacion esperada del paquete en equipos con OpenCode +- ✅ Documentado un TODO completo desde el estado actual hasta el producto terminado al 100 por 100 + +#### Estado final +- Queda creado el contenedor del proyecto `opencode-browser-tool` para desarrollar la herramienta de browser externa. +- Queda definido un `Plan de desarrollo` base para guiar la implementacion de la v1 y la continuidad hacia versiones posteriores. +- Queda creado un `TODO` integral que cubre el recorrido completo hasta producto terminado. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/PLAN_DE_DESARROLLO.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 10 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.4 +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `fase-0-estructura-base-opencode-browser-tool-2026-04-23` + +#### Trabajo realizado +- ✅ Cerrada la definicion base de la `Fase 0` del proyecto `opencode-browser-tool` +- ✅ Fijado el stack de la v1: `Node.js 20+`, `TypeScript`, `Playwright`, `Chromium`, `@modelcontextprotocol/sdk` y `MCP` por `stdio` +- ✅ Fijada la estructura inicial del proyecto y creada en disco con carpetas para `src`, `docs`, `scripts`, `artifacts`, `browser`, `tools` y `types` +- ✅ Creado `README.md` principal del proyecto +- ✅ Creados `package.json`, `tsconfig.json`, `.gitignore`, `install.sh`, `check.sh`, `opencode.mcp.example.json` y un esqueleto inicial en `src/server.ts` +- ✅ Añadidas al `Plan de desarrollo` las tools MCP minimas de la v1 y la estructura fijada del proyecto +- ✅ Actualizado el `TODO` para reflejar el cierre de la `Fase 0` y la definicion de las tools MCP minimas +- ✅ Ejecutada comprobacion base del proyecto con `check.sh`, confirmando `Node`, `npm` y la estructura minima disponible + +#### Estado final +- La base documental y estructural del proyecto queda preparada para comenzar la implementacion del `MCP server` de la v1. +- El proyecto todavia no tiene dependencias instaladas ni servidor MCP funcional; esa implementacion corresponde al siguiente bloque de trabajo. +- Queda lista una plantilla de configuracion MCP de ejemplo para conectar OpenCode cuando el servidor este compilado. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/.gitignore` +- `opencode-browser-tool/README.md` +- `opencode-browser-tool/check.sh` +- `opencode-browser-tool/install.sh` +- `opencode-browser-tool/opencode.mcp.example.json` +- `opencode-browser-tool/package.json` +- `opencode-browser-tool/tsconfig.json` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/PLAN_DE_DESARROLLO.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 11 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `fase-1-mcp-playwright-base-2026-04-23` + +#### Trabajo realizado +- ✅ Registrada la decision de v1 para `Chromium managed by Playwright` y su instalacion en `install.sh` +- ✅ Anotada en plan y TODO la evolucion futura hacia modo opcional `system browser` +- ✅ Implementado servidor MCP funcional por `stdio` en `opencode-browser-tool/src/server.ts` +- ✅ Implementadas las tools MCP minimas de la v1: `browser_open`, `browser_close`, `browser_navigate`, `browser_click`, `browser_type`, `browser_press`, `browser_scroll`, `browser_wait`, `browser_snapshot`, `browser_evaluate` +- ✅ Conectada la capa MCP con Playwright mediante `BrowserManager` en `opencode-browser-tool/src/browser/manager.ts` +- ✅ Configurado `Chromium` en modo visible por defecto en `browser_open` +- ✅ Añadida gestion basica de sesion de browser, navegacion, interaccion, espera y screenshot en artifacts +- ✅ Instaladas dependencias de proyecto y navegador con `npm install` y `npx playwright install chromium` +- ✅ Compilado el proyecto con `npm run build` sin errores +- ✅ Validado arranque base del servidor compilado (`node dist/server.js`) y estado de entorno con `check.sh` + +#### Estado final +- La Fase 1 queda iniciada con servidor MCP funcional y conectado a Playwright. +- Quedan pendientes dentro de Fase 1 la validacion de deteccion/uso desde OpenCode y la primera navegacion real ejecutada via tool MCP. +- La base actual ya permite avanzar al test de integracion real OpenCode -> MCP server -> Chromium. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/README.md` +- `opencode-browser-tool/docs/PLAN_DE_DESARROLLO.md` +- `opencode-browser-tool/docs/TODO.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` + +### Sesion 12 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `manual-uso-tools-browser-2026-04-23` + +#### Trabajo realizado +- ✅ Creado el documento `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- ✅ Documentadas de forma concisa y operativa todas las tools actuales de la herramienta browser +- ✅ Incluidos para cada tool: objetivo, parametros, ejemplos JSON y notas de uso +- ✅ Añadido flujo recomendado de operacion de la herramienta de inicio a cierre de sesion +- ✅ Añadidos ejemplos de peticiones reales que el usuario puede pedirle a OpenCode +- ✅ Incluida seccion de limites actuales de la version para alinear expectativas de uso + +#### Estado final +- Queda disponible un manual operativo de referencia para que el usuario/operario sepa exactamente que puede pedir y como usar cada tool del browser. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` + +### Sesion 13 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `enlace-manual-y-panorama-fases-2026-04-23` + +#### Trabajo realizado +- ✅ Añadido al `README.md` principal del subproyecto el enlace a `docs/manual_de_uso_heramienta.md` +- ✅ Ajustado el `TODO.md` para reflejar estado real de avance en Fase 4 (scripts y plantilla MCP ya creados) + +#### Estado final +- El manual de uso queda visible desde el punto de entrada del proyecto. +- El tablero de fases refleja mejor el estado actual para revisar pendientes con claridad. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/README.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 14 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `prueba-integracion-real-cierre-fase1-2026-04-23` + +#### Trabajo realizado +- ✅ Creada configuracion local MCP del proyecto en `opencode-browser-tool/.opencode/opencode.json` +- ✅ Validada deteccion de servidor MCP desde el proyecto con `opencode mcp list`, mostrando `browser-tool` conectado +- ✅ Ejecutada prueba real de integracion `OpenCode -> MCP -> Playwright -> Chromium` con `opencode run` +- ✅ Probada navegacion real a URL local `http://127.0.0.1:4173` usando servidor local temporal (`python3 -m http.server`) +- ✅ Confirmadas tool calls de la secuencia de prueba: `browser_open`, `browser_navigate`, `browser_wait`, `browser_snapshot`, `browser_close` +- ✅ Generada evidencia de la prueba en artifacts: `opencode-browser-tool/artifacts/2026-04-23T14-49-17-661Z-fase1-local.png` +- ✅ Cerrados los 2 pendientes restantes de Fase 1 en `opencode-browser-tool/docs/TODO.md` + +#### Estado final +- Fase 1 cerrada: la herramienta ya se detecta desde OpenCode y ejecuta una navegacion local real con evidencia. +- El siguiente bloque natural de trabajo es Fase 2 (interaccion efectiva con UI). + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/.opencode/opencode.json` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 15 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `espera-nativa-automatica-browser-tool-2026-04-23` + +#### Trabajo realizado +- ✅ Implementada logica de estabilizacion automatica en `BrowserManager` para reducir esperas manuales +- ✅ Añadido tracking de actividad de red (`request`, `requestfinished`, `requestfailed`) en contexto Playwright +- ✅ Añadida estrategia de espera nativa tras acciones clave (`navigate`, `click`, `type`, `press`, `scroll`, `evaluate`) +- ✅ Añadida salida segura por tiempo maximo para evitar esperas ciclicas o infinitas +- ✅ Actualizada documentacion funcional en `manual_de_uso_heramienta.md` para reflejar el comportamiento de espera automatica +- ✅ Ajustado `PLAN_DE_DESARROLLO.md` para dejar explicita esta capacidad en v1 +- ✅ Ejecutada prueba real de integracion sin `browser_wait` explicito y validado flujo completo con snapshot generado +- ✅ Marcada como completada en TODO la tarea de esperas utiles de Fase 2 + +#### Estado final +- La herramienta ya incorpora espera nativa y no depende de que el usuario pida `wait` en flujos comunes. +- `browser_wait` permanece disponible para sincronizacion explicita cuando se quiera una condicion concreta. +- Queda verificado el comportamiento automatico con evidencia en artifacts. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/PLAN_DE_DESARROLLO.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 16 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `ejecucion-real-login-sitio-externo-2026-04-23` + +#### Trabajo realizado +- ✅ Ejecutada prueba real solicitada por usuario usando exclusivamente la tool `browser-tool` +- ✅ Abierto navegador visible y navegacion a `https://app.drmoralestorres.com/` +- ✅ Login completado con las credenciales facilitadas por el usuario +- ✅ Verificada entrada al panel interno en `https://app.drmoralestorres.com/dashboard` +- ✅ Captura tomada dentro de la app autenticada +- ✅ Navegador cerrado al finalizar la secuencia + +#### Estado final +- Prueba completada correctamente con estado `OK`. +- Evidencia generada en `opencode-browser-tool/artifacts/2026-04-23T15-08-20-669Z-drmoralestorres-dashboard.png`. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` + +### Sesion 17 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `auto-descubrimiento-mcp-y-quickstart-2026-04-23` + +#### Trabajo realizado +- ✅ Añadida nueva tool MCP `browser_help` en `opencode-browser-tool/src/server.ts` +- ✅ `browser_help` devuelve capacidades, defaults, comportamiento de espera nativa, notas y ejemplos +- ✅ Ampliado `manual_de_uso_heramienta.md` con seccion de errores tipicos y recuperacion +- ✅ Creado `opencode-browser-tool/docs/QUICKSTART.md` con instalacion y primera prueba minima +- ✅ Actualizado `opencode-browser-tool/README.md` para incluir enlace al quickstart +- ✅ Compilacion validada con `npm run build` +- ✅ Validado uso real de `browser_help` desde OpenCode con `opencode run` + +#### Estado final +- La herramienta ya ofrece auto-descubrimiento operativo para agentes nuevos via MCP (`browser_help`). +- Queda reforzada la documentacion para instalacion rapida y recuperacion de fallos comunes. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/README.md` + +### Sesion 18 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `onboarding-inicial-y-browser-health-2026-04-23` + +#### Trabajo realizado +- ✅ Reforzado onboarding para agentes nuevos en `README.md` con recomendacion explicita de ejecutar `browser_help` y `browser_health` al iniciar sesion +- ✅ Implementada nueva tool MCP `browser_health` en `opencode-browser-tool/src/server.ts` +- ✅ `browser_health` devuelve estado runtime (`isOpen`, `currentUrl`, `inflight`, `lastActivityAgoMs`) y artifacts recientes +- ✅ Añadida logica de lectura de artifacts recientes con limite configurable +- ✅ Extendida documentacion en `manual_de_uso_heramienta.md` con uso de `browser_health` +- ✅ Actualizado `QUICKSTART.md` para incluir paso de auto-descubrimiento (`browser_help` + `browser_health`) +- ✅ Compilacion validada con `npm run build` +- ✅ Validada ejecucion real desde OpenCode de `browser_help` y `browser_health` + +#### Estado final +- Un agente nuevo en otra maquina ya tiene un camino claro y corto para descubrir capacidades y estado operativo de la herramienta desde el primer uso. +- La tool `browser_health` queda disponible para diagnostico rapido sin depender de contexto previo. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/README.md` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` + +### Sesion 19 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `fase2-hover-select-y-validacion-e2e-2026-04-23` + +#### Trabajo realizado +- ✅ Implementadas nuevas acciones de interaccion: `browser_hover` y `browser_select` +- ✅ Integradas ambas tools en `server.ts` (schema + handlers + exposicion en `browser_help`) +- ✅ Extendida la capa `BrowserManager` con metodos `hover` y `select` con estabilizacion automatica nativa +- ✅ Actualizado `manual_de_uso_heramienta.md` con ejemplos y parametros de `browser_hover` y `browser_select` +- ✅ Añadido fixture local `scripts/fixture_select_hover.html` para validacion reproducible +- ✅ Ejecutada prueba real end-to-end contra fixture local con secuencia: open -> navigate -> hover -> select -> evaluate -> snapshot -> close +- ✅ Verificada evidencia en `opencode-browser-tool/artifacts/2026-04-23T17-14-58-568Z-fixture-select-hover.png` +- ✅ Actualizado `TODO.md` para reflejar estado real de avances en Fase 2/Fase 3 + +#### Estado final +- El bloque de acciones base de Fase 2 queda cubierto (incluyendo `select` y `hover`). +- La herramienta queda lista para seguir con lectura estructurada de DOM/estado y estandarizacion final del esquema de respuesta. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/scripts/fixture_select_hover.html` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 20 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `dom-ui-query-y-evaluate-flexible-2026-04-23` + +#### Trabajo realizado +- ✅ Implementada nueva tool `browser_query` para lectura estructurada DOM/UI sin JavaScript manual +- ✅ `browser_query` soporta modos: `text`, `html`, `value`, `exists`, `visible`, `enabled`, `count`, `attributes` +- ✅ Integrada `browser_query` en `browser_help` y en handlers MCP de `server.ts` +- ✅ Mejorada `browser_evaluate` para aceptar funcion o expresion directa, incluyendo resultados ya evaluados (IIFE) +- ✅ Confirmado comportamiento de inyeccion/modificacion en runtime mediante `browser_evaluate` +- ✅ Actualizado `manual_de_uso_heramienta.md` con `browser_query` y nuevas notas de `browser_evaluate` +- ✅ Ejecutadas pruebas reales con fixture local para validar `browser_query` y modos flexibles de `browser_evaluate` +- ✅ Actualizado `TODO.md` marcando completada la lectura DOM/UI en Fase 2 + +#### Estado final +- La herramienta ya cubre inspeccion DOM/UI estructurada y ejecucion/inyeccion JS en runtime de forma usable para agentes en pruebas complejas. +- Queda pendiente de Fase 2 la estandarizacion final del esquema de respuesta por accion. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 21 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `fase2-respuesta-estandar-final-y-doc-2026-04-23` + +#### Trabajo realizado +- ✅ Estandarizado el esquema de respuesta para todas las tools MCP en `server.ts` +- ✅ Formato unificado de exito: `{ ok, tool, data, state }` +- ✅ Formato unificado de error: `{ ok:false, tool, error:{code,message}, state }` +- ✅ Añadida clasificacion base de errores (`SESSION_NOT_OPEN`, `TOOL_EXECUTION_ERROR`) +- ✅ Actualizado `browser_help` para exponer el `responseSchema` oficial y onboarding recomendado +- ✅ Mejorada compatibilidad de `browser_evaluate` para expresion, funcion y resultados evaluados (IIFE) +- ✅ Validado por ejecucion real en OpenCode que `browser_help`, `browser_open`, `browser_navigate`, `browser_query` y `browser_close` devuelven `data` + `state` +- ✅ Actualizado `manual_de_uso_heramienta.md` con esquema estandar de respuesta +- ✅ Marcado en `TODO.md` el cierre del ultimo pendiente fuerte de Fase 2 + +#### Estado final +- Fase 2 queda cerrada con acciones de UI, lectura DOM/UI, ejecucion JS e interfaz de respuesta consistente. +- La siguiente prioridad natural es Fase 3 (logs de consola, errores de pagina y reporte diagnostico). + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 22 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `fase3-diagnostico-consola-y-pageerror-2026-04-23` + +#### Trabajo realizado +- ✅ Aclarado alcance de inyeccion JS: uso diagnostico en runtime, ideal para probar hipotesis de fallo +- ✅ Implementadas capturas de diagnostico en `BrowserManager`: eventos `console`, `pageerror` y `requestfailed` +- ✅ Añadidas tools MCP nuevas: `browser_diagnostics` y `browser_diagnostics_clear` +- ✅ Integradas tools de diagnostico en `browser_help` (capabilities y ejemplo de uso) +- ✅ Creado fixture de prueba `scripts/fixture_diagnostics.html` con logs y error JS controlado +- ✅ Ejecutada validacion real con OpenCode: se capturaron eventos de consola y error de pagina +- ✅ Implementada lectura DOM/UI estructurada con `browser_query` y documentada +- ✅ Mejorada `browser_evaluate` para soportar funcion, expresion e IIFE sin fallos de forma +- ✅ Actualizado esquema de respuesta estandar y documentacion de uso +- ✅ Marcados en TODO los avances de Fase 3: consola y errores de pagina completados + +#### Estado final +- Fase 3 avanza con diagnostico operativo real: ya hay captura de consola y de errores JS de pagina. +- Quedan de Fase 3: evaluar video/trace y cerrar formato final de reporte para el agente. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/docs/TODO.md` +- `opencode-browser-tool/scripts/fixture_diagnostics.html` + +### Sesion 23 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `video-trace-manual-bajo-demanda-y-ajustes-2026-04-23` + +#### Trabajo realizado +- ✅ Implementado soporte manual bajo demanda de `video` y `trace` en `browser_open` +- ✅ Nuevos parametros de apertura: `recordVideo`, `recordTrace`, `recordLabel` +- ✅ Generacion de artifacts al cerrar sesion en `browser_close` (`videoPath` y `tracePath`) +- ✅ Actualizado `browser_help` con defaults de recording y ejemplo de uso +- ✅ Actualizada documentacion de `browser_open` y `QUICKSTART` para grabacion bajo demanda +- ✅ Validacion real de recording: generado `.webm` y `trace.zip` en artifacts con flujo OpenCode -> MCP +- ✅ Corregido comportamiento de `browser_wait` para `for: timeout` usando `value` como milisegundos cuando se provee +- ✅ Validado en ejecucion real que `browser_wait` devuelve `waitedMs: 500` cuando `value="500"` +- ✅ Marcado en TODO el cierre de "Evaluar inclusion de video o trace en la v1" + +#### Estado final +- Video/trace quedan operativos para desarrollo interno bajo activacion manual por ejecucion. +- Se mantiene pendiente la politica operativa avanzada (auto en fallo, retencion, etc.) para fases posteriores. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 24 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `telemetria-documentada-y-browser-report-v1-2026-04-23` + +#### Trabajo realizado +- ✅ Documentada la telemetria en tiempo real como mejora posterior en `docs/idea_y_funcionamiento_herramienta_browser.md` +- ✅ Añadida descripcion de valor, costes, cobertura actual sin telemetria y enfoque recomendado para fases futuras +- ✅ Implementada nueva tool `browser_report` en `opencode-browser-tool/src/server.ts` +- ✅ `browser_report` soporta formato `json`, `markdown` y `both`, con opcion de guardado en archivos +- ✅ Añadido registro interno de pasos de ejecucion para consolidar reportes (tool, input, estado, duracion, error) +- ✅ Integrado `browser_report` en `browser_help` (capabilities + ejemplo) +- ✅ Actualizado manual con uso de `browser_report` y actualizado quickstart con seccion de reporte consolidado +- ✅ Implementado y validado recording manual (`recordVideo`, `recordTrace`, `recordLabel`) y export de artifacts en `browser_close` +- ✅ Corregido `browser_wait` para respetar `value` como milisegundos en modo `timeout` +- ✅ Validada ejecucion real end-to-end de `browser_report` con guardado de archivos `.json` y `.md` + +#### Estado final +- Queda disponible un flujo hibrido de reporte: la tool consolida reporte base y el agente puede emitir resumen contextual en TUI. +- Fase 3 queda cerrada al 100 por 100 en el tablero actual. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `docs/idea_y_funcionamiento_herramienta_browser.md` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/docs/TODO.md` + +### Sesion 25 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `default-savetofile-persistente-y-override-temporal-2026-04-23` + +#### Trabajo realizado +- ✅ Implementado default persistente para reportes con `saveToFile=true` al instalar +- ✅ Añadida configuracion persistente en `opencode-browser-tool/config/browser-tool.config.json` +- ✅ Añadida nueva tool `browser_config` para leer/cambiar configuracion persistente en runtime +- ✅ Integrado en `browser_report` el comportamiento por defecto desde config y override temporal por llamada +- ✅ Integrado en `browser_help` el bloque de capacidades/config y ejemplos de uso temporal vs persistente +- ✅ Actualizado manual con la nueva tool `browser_config` y el comportamiento correcto de `browser_report.saveToFile` +- ✅ Actualizado quickstart y README con el nuevo flujo de configuracion +- ✅ Validado en ejecucion real los 3 casos: + - default activo (`saveToFile=true`) guarda archivo + - override temporal (`saveToFile=false`) no guarda y no persiste + - cambio persistente (`browser_config set`) cambia el default para llamadas siguientes +- ✅ Restaurado al final el default persistente a `reportDefaultSaveToFile=true` + +#### Estado final +- Quedan aplicados los 3 puntos acordados: default activo, override temporal no persistente y configuracion persistente para operario. +- `browser_report` queda listo para uso operativo con control fino por ejecucion y por configuracion global. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/config/browser-tool.config.json` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/README.md` + +### Sesion 26 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `registro-situaciones-instrucciones-operativas-2026-04-23` + +#### Trabajo realizado +- ✅ Creado `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` +- ✅ Añadidas instrucciones operativas al inicio para que cualquier agente documente errores/situaciones de forma consistente +- ✅ Incluida plantilla estandar de entrada con campos de contexto, evidencia, causa, accion, validacion y estado +- ✅ Añadida entrada inicial de arranque del registro + +#### Estado final +- Queda disponible un fichero unico para acumular hallazgos reales durante desarrollo y pruebas con trazabilidad clara. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 27 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `readme-enlace-registro-situaciones-2026-04-23` + +#### Trabajo realizado +- ✅ Añadido `docs/REGISTRO_SITUACIONES.md` al bloque de documentacion principal en el `README` de la herramienta + +#### Estado final +- El fichero de registro de situaciones queda localizable rapidamente desde `opencode-browser-tool/README.md`. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/README.md` + +### Sesion 28 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `prueba-google-paginacion-y-bloqueo-antibot-2026-04-23` + +#### Trabajo realizado +- ✅ Ejecutada prueba solicitada de Google con objetivo de paginacion (`Siguiente`) y deteccion de dominio `barranquismogranada.com` +- ✅ Implementado flujo de busqueda, consentimiento, envio de consulta y analisis de resultados +- ✅ Detectado bloqueo anti-bot de Google con redireccion a `https://www.google.com/sorry/index` +- ✅ Reintento en modo visible para minimizar sesgo de headless; bloqueo persistente +- ✅ Registrada la incidencia y evidencia en `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +#### Estado final +- La prueba no puede considerarse valida en este entorno por bloqueo anti-bot externo al flujo funcional de la tool. +- Queda en analisis con evidencia guardada para definir estrategia de continuidad en pruebas de buscadores externos. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 29 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `observacion-intermedia-y-autosnapshot-en-error-2026-04-23` + +#### Trabajo realizado +- ✅ Implementada captura automatica al fallar una tool (`autoSnapshot`) con observacion de pagina adjunta +- ✅ Añadida tool `browser_observe` para diagnostico en mitad de flujo (estado + screenshot opcional) +- ✅ Añadida tool `browser_handle_consent` para intentar resolver rapidamente banners de consentimiento +- ✅ Integradas nuevas capacidades en `browser_help` (recovery) +- ✅ Actualizada documentacion en `manual_de_uso_heramienta.md` y `QUICKSTART.md` +- ✅ Validado en ejecucion real que un error de `browser_wait` devuelve `autoSnapshot` y `observation` +- ✅ Validado `browser_observe` con captura intermedia (`mid-check`) +- ✅ Actualizada entrada de situacion de Google para reflejar la mejora aplicada + +#### Estado final +- La herramienta ya permite inspeccion intermedia y no corta flujos "a ciegas" cuando hay bloqueo o timeout. +- Queda pendiente resolver casos anti-bot de terceros con estrategia de handoff/validacion manual cuando aplique. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 30 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `retest-google-bloqueado-humano-con-evidencia-2026-04-23` + +#### Trabajo realizado +- ✅ Reintentada prueba de Google solicitada con flujo mejorado (consent reject, observacion intermedia y diagnostico) +- ✅ Consentimiento resuelto con `browser_handle_consent` (`Rechazar todo`) +- ✅ Detectado bloqueo por verificacion humana en `https://www.google.com/sorry/index` +- ✅ Capturada evidencia automatica de bloqueo en error (`autoSnapshot`) +- ✅ Registrada incidencia y evidencia en `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +#### Estado final +- Prueba no concluyente por bloqueo anti-bot de tercero (Google), no por fallo interno de las tools. +- Se requiere handoff humano para superar challenge si se desea continuar esta prueba exacta en Google. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 31 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `reintento-google-con-human-check-explicito-2026-04-23` + +#### Trabajo realizado +- ✅ Añadida capacidad de intento de verificacion humana (`browser_handle_human_check`) y desplegada en MCP +- ✅ Reintentada prueba de Google con flujo completo incluyendo intentos de clic en human-check +- ✅ Ejecutado `browser_observe` antes y despues del intento para evidencia visual de mitad de flujo +- ✅ Confirmado bloqueo persistente en `sorry/index` sin control clicable resoluble automaticamente en esta sesion +- ✅ Registrada evidencia completa en `REGISTRO_SITUACIONES.md` + +#### Estado final +- La herramienta ya intenta automaticamente el paso de human-check cuando corresponde, pero el challenge de Google en esta sesion no es resoluble por clic simple expuesto. +- Para pasar esta prueba exacta en este entorno, queda requerido handoff humano directo en la sesion. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 32 (2026-04-23) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `validacion-visual-click-human-check-y-evidencia-v2-2026-04-23` + +#### Trabajo realizado +- ✅ Reejecutada prueba en modo visible para observar en directo el intento de clic humano +- ✅ Ampliada logica de `browser_handle_human_check` con mas selectores e iframes de checkbox reCAPTCHA +- ✅ Generadas capturas antes/despues del intento (`before-human-check-v2`, `after-human-check-v2`) +- ✅ Grabado video de ejecucion visible para ver la secuencia real +- ✅ Confirmado por salida de tool que no hubo control clicable (`clicked=false`) en este challenge +- ✅ Registrada la evidencia completa en `REGISTRO_SITUACIONES.md` + +#### Estado final +- Queda verificado que la tool si intenta el clic; en este challenge concreto no encuentra un elemento clicable resoluble automaticamente. +- Para completar la prueba exacta de Google en este entorno sigue haciendo falta handoff manual. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 33 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `continuidad-validacion-fase4-y-siguiente-paso-2026-04-24` + +#### Trabajo realizado +- ✅ Revisado estado actual del proyecto y pendientes operativos en `docs/TODO.md` +- ✅ Verificada salud local del proyecto con `npm run check` +- ✅ Verificada compilacion actual con `npm run build` +- ✅ Actualizado `docs/TODO.md` cerrando pendientes ya cumplidos de Fase 4 (documentacion paso a paso y despliegue desde carpeta unica) +- ✅ Añadida guia operativa para cerrar la validacion en entorno limpio: `docs/VALIDACION_ENTORNO_LIMPIO.md` +- ✅ Actualizado `README.md` para incluir el nuevo documento de validacion + +#### Estado final +- La base de instalacion/empaquetado queda mas alineada con el estado real del repositorio. +- Permanece pendiente la validacion en entorno limpio con OpenCode para cerrar completamente Fase 4. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/TODO.md` +- `opencode-browser-tool/docs/VALIDACION_ENTORNO_LIMPIO.md` +- `opencode-browser-tool/README.md` + +### Sesion 34 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `continuidad-prueba-google-cursor-scroll-y-contexto-persistente-2026-04-24` + +#### Trabajo realizado +- ✅ Reforzada `browser_handle_human_check` con mas selectores, intento en pagina principal y fallback por coordenadas +- ✅ Mejorado `browser_scroll` para devolver metricas reales de desplazamiento (`startY/endY/movedY/maxY` y `movedX/movedY` por elemento) +- ✅ Ajustado `browser_hover` para mover puntero antes del hover y mantener coherencia visual +- ✅ Creado fixture largo real para validar scroll: `scripts/fixture_scroll_long.html` +- ✅ Ejecutada prueba visible de cursor+scroll con evidencia (screenshot y video) +- ✅ Ejecutado retest Google visible: consentimiento OK, clic humano reportado (`clicked=true` en ancla reCAPTCHA), pero permanencia en `sorry/index` en ese intento +- ✅ Ejecutado retest objetivo completo v4 (Google + paginacion): bloqueo persistente en `sorry/index` pese a clic reportado +- ✅ Añadido archivo persistente de continuidad: `docs/CONTEXTO_ACTIVO_BROWSER.md` +- ✅ Añadido skill manual de continuidad: `docs/SKILL_CONTINUIDAD_BROWSER.md` +- ✅ Registradas incidencias/resultados nuevos en `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +#### Estado final +- Queda resuelta la duda de si la tool puede clicar: hay evidencia de clic efectivo en reCAPTCHA en el retest v3. +- El objetivo activo sigue abierto: completar salida de `sorry/index` y terminar paginacion hasta ubicar el dominio objetivo. +- Se deja continuidad persistente explicita para evitar perdida de foco por compactacion. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `docs/CONTEXTO_ACTIVO_BROWSER.md` +- `docs/SKILL_CONTINUIDAD_BROWSER.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/scripts/fixture_scroll_long.html` +- `opencode-browser-tool/scripts/google_pagination_v4.mjs` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 35 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `dual-browser-testing-system-y-perfil-persistente-2026-04-24` + +#### Trabajo realizado +- ✅ Implementado soporte dual en `browser_open`: `browserKind=testing|system` +- ✅ Implementado soporte de perfil persistente por sesion (`persistentProfile`, `userDataDir`) +- ✅ Añadido soporte de defaults persistentes de navegador en `browser_config` +- ✅ Mantenido default de grabacion en `recordVideo=false` y reforzado en `browser_help` +- ✅ Actualizada `browser_help` con capacidades nuevas (modo system y perfil persistente) +- ✅ Actualizada documentacion de uso (`manual_de_uso_heramienta.md`, `QUICKSTART.md`, `README.md`) +- ✅ Actualizado `config/browser-tool.config.json` con bloque `browser` +- ✅ Validada compilacion con `npm run build` + +#### Estado final +- La tool ya permite elegir entre navegador testing y navegador del sistema, y entre perfil efimero o persistente. +- El objetivo de continuidad (historial de usuario) queda habilitado tecnicamente mediante `persistentProfile + userDataDir`. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/config/browser-tool.config.json` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/README.md` + +### Sesion 36 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `retest-google-organico-con-perfil-persistente-v7-2026-04-24` + +#### Trabajo realizado +- ✅ Ejecutada prueba solicitada de Google con resultados organicos usando perfil persistente +- ✅ Lanzada sesion con `browserKind=testing`, `persistentProfile=true`, `userDataDir=/home/pancho/.chromium-perfil-google` +- ✅ Flujo completado con busqueda, scroll visible, paginacion y deteccion de dominio objetivo +- ✅ Registrado resultado en `REGISTRO_SITUACIONES.md` y actualizado contexto activo + +#### Estado final +- Resultado organico obtenido: dominio en pagina 2, posicion 6 (rank global 16 en conteo del script). +- Persistio challenge inicial `sorry/index`, pero la sesion avanzo a `/search` y permitio completar la prueba. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `docs/CONTEXTO_ACTIVO_BROWSER.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` + +### Sesion 37 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `prueba-google-es-organico-click-y-scroll-v8d-2026-04-24` + +#### Trabajo realizado +- ✅ Ejecutada prueba en `google.es` con dominio organico (`opcion 1`) y esperas cortas (max 3s extra) +- ✅ Localizado dominio objetivo en resultados organicos y contabilizado puesto +- ✅ Clic realizado sobre el resultado encontrado para cargar la web destino +- ✅ Scroll suave ejecutado hasta el final de la pagina destino +- ✅ Registrada evidencia y resultado en `REGISTRO_SITUACIONES.md` + +#### Estado final +- Prueba completada correctamente segun criterios solicitados por el usuario. +- Resultado organico obtenido en esta corrida: pagina 2, posicion 7 (rank global 17 en conteo del script). + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` +- `opencode-browser-tool/scripts/google_es_v8d.mjs` + +### Sesion 38 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `verbose-default-y-estabilidad-previa-a-pasos-sensibles-2026-04-24` + +#### Trabajo realizado +- ✅ Implementado `verbose` por defecto en `browser_open` con opcion de desactivar +- ✅ Implementado overlay visual de estado (`verboseOverlay`) para modo visible +- ✅ Implementado delay humano configurable (`interactionDelayMinMs/MaxMs`) por defecto 1-3s +- ✅ Aplicada guarda de estabilidad previa a pasos sensibles (espera de pagina estable antes de accionar) +- ✅ Extendida configuracion persistente en `browser_config` para verbose/delay +- ✅ Actualizadas `browser_help`, `manual_de_uso_heramienta.md`, `QUICKSTART.md` y `README.md` +- ✅ Validado build y smoke test local de click/scroll con pasos recientes en `browser_health` + +#### Estado final +- El operario ya puede ver en tiempo real que esta haciendo la tool (verbose activo por defecto). +- Se reduce riesgo de race condition al esperar estabilidad antes de acciones sensibles. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/src/browser/manager.ts` +- `opencode-browser-tool/src/server.ts` +- `opencode-browser-tool/config/browser-tool.config.json` +- `opencode-browser-tool/docs/manual_de_uso_heramienta.md` +- `opencode-browser-tool/docs/QUICKSTART.md` +- `opencode-browser-tool/README.md` + +### Sesion 39 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `preservacion-pendientes-y-plan-cierre-instalable-2026-04-24` + +#### Trabajo realizado +- ✅ Guardados pendientes inmediatos solicitados para evitar perdida por compactacion +- ✅ Creado documento de cierre de instalable v1: `docs/PLAN_CIERRE_INSTALABLE.md` +- ✅ Actualizado `docs/TODO.md` con tareas inmediatas (prueba final, cierre instalable Linux, evaluacion Windows/npm) +- ✅ Actualizado contexto activo con referencia explicita al plan de cierre preservado +- ✅ Actualizado indice documental en `README.md` + +#### Estado final +- Queda preservada la hoja de ruta inmediata: prueba final + empaquetado portable Linux + evaluacion Windows. +- El plan de cierre queda persistente y listo para ejecucion en la siguiente fase. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `docs/CONTEXTO_ACTIVO_BROWSER.md` +- `opencode-browser-tool/docs/TODO.md` +- `opencode-browser-tool/docs/PLAN_CIERRE_INSTALABLE.md` +- `opencode-browser-tool/README.md` + +### Sesion 40 (2026-04-24) - Agente modulo Browser Opencode +**Agente:** **Agente modulo Browser Opencode** +**Modelo:** openai/gpt-5.3-codex +**OpenCode session_id:** `ses_248d8d7b9ffeXSm1L7BDQsWLOw` +**Referencia de sesion:** `prueba-google-puertas-cortafuegos-ranking-2026-04-24` + +#### Trabajo realizado +- ✅ Ejecutada prueba solicitada en Google para query `puertas cortafuegos de madera` +- ✅ Paginados resultados hasta localizar dominio `puertastecnicasbcn.com` +- ✅ Acceso a la URL encontrada y captura de evidencia +- ✅ Registrado resultado en `REGISTRO_SITUACIONES.md` + +#### Estado final +- Dominio localizado en pagina 3, posicion 4 organica (rank global 23 en conteo del script). +- Evidencia generada en artifacts con snapshot de destino. + +#### Archivos modificados +- `docs/HISTORIAL_SESIONES.md` +- `opencode-browser-tool/docs/REGISTRO_SITUACIONES.md` +- `opencode-browser-tool/scripts/google_es_puertas_rank.mjs` + +### Sesion ejemplo (Fecha - Hora) - [Nombre Agente] +**Agente:** **[Nombre asignado]** +**Modelo:** [Modelo AI utilizado] +**OpenCode session_id:** `[session_id de OpenCode]` +**Referencia de sesion:** `[ID o referencia interna]` + +#### Trabajo realizado +- ✅ Item completado 1 +- ✅ Item completado 2 +- 🚧 Item en progreso + +#### Estado final +- Resumen del estado al cerrar la sesión +- Pendientes críticos + +#### Archivos modificados +- `path/to/file1` +- `path/to/file2` + +--- + +## Notas de uso + +### Regla de registro automatico + +Todo agente que trabaje en este workspace debe actualizar este historial sin esperar a que el usuario lo pida cuando ocurra una accion relevante dentro de su sesion o interaccion. + +No solo deben registrarse grandes resultados finales. Tambien deben registrarse acciones relevantes en cuanto ocurren. + +Cada registro de interaccion, accion, tarea, cambio, hallazgo o decision debe quedar asociado de forma visible al nombre del agente que lo realizo, y no solo a su `OpenCode session_id`. + +Se considera accion relevante cualquier hecho que cambie el estado del trabajo, cree nuevo contexto o deje una huella importante para futuras continuaciones. + +Esto incluye, entre otros casos: + +- crear un documento nuevo +- iniciar una auditoria o investigacion tecnica +- abrir un subproyecto o una nueva linea de trabajo +- registrar un pendiente importante +- tomar una decision tecnica o de arquitectura +- descubrir un hallazgo que cambie la direccion del analisis +- modificar archivos relevantes +- dejar una conclusion parcial que afecte los siguientes pasos + +No debe esperarse al final de la interaccion para registrar todo junto si durante ella ya han ocurrido acciones significativas que conviene dejar trazadas. + +### Para registrar una interaccion dentro de una sesion o abrir una nueva sesion + +Si el agente ya tiene una sesion registrada, debe seguir añadiendo en este historial las acciones relevantes que vayan ocurriendo dentro de nuevas interacciones asociadas a esa misma `session_id`. + +Si aparece una sesion nueva de OpenCode para otro agente o para una nueva conversacion persistente, debe crearse una nueva entrada documental y actualizar antes el indice de agentes si hace falta. + +Al ampliar el historial, no basta con dejar solo el `session_id`: debe figurar tambien el nombre del agente responsable de esa interaccion o bloque registrado para que el usuario pueda identificar rapidamente quien hizo cada cosa. + +```markdown +### Sesion X (fecha) - [Nombre del Agente] +**Agente:** **[Nombre asignado]** +**Modelo:** [Modelo usado] +**OpenCode session_id:** `[session_id de OpenCode]` +**Referencia de sesion:** `[ID o referencia interna]` + +#### Trabajo realizado +- Item 1 +- Item 2 + +#### Estado final +- Estado 1 +``` + +### Para asignar nombre a un agente + +Al inicio de una conversación nueva o al retomar una existente, indica: +> "Tu nombre es [Nombre del Agente] y te encargas de [responsabilidad]" + +Antes de registrarse en el indice de agentes o de documentar una nueva interaccion, el agente debe obtener correctamente su `session_id` usando `docs/sesion_actual_opencode.md`. + +El registro correcto de un agente en este documento debe incluir siempre estas tres piezas juntas: + +- nombre del agente +- `OpenCode session_id` +- responsabilidad o rol principal + +El nombre del agente es imprescindible para que el usuario pueda reconocer facilmente quien es quien. El `session_id` es imprescindible para que el agente pueda identificarse con precision tecnica dentro del workspace. + +### Criterio practico + +Si una accion mereceria ser recordada mañana para entender que paso en la sesion, entonces debe registrarse aqui hoy, aunque el usuario no lo pida expresamente. + +--- + +## Agentes activos + +### [Nombre Agente 1] +- **Responsabilidad:** Descripción detallada de responsabilidades. +- **Estado:** Activo/Inactivo +- **Trabajo principal:** Enfoque actual del agente. diff --git a/docs/contexto_workspace/SKILL_CONTINUIDAD_BROWSER.md b/docs/contexto_workspace/SKILL_CONTINUIDAD_BROWSER.md new file mode 100644 index 0000000..a0677a3 --- /dev/null +++ b/docs/contexto_workspace/SKILL_CONTINUIDAD_BROWSER.md @@ -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. diff --git a/docs/contexto_workspace/idea_y_funcionamiento_herramienta_browser.md b/docs/contexto_workspace/idea_y_funcionamiento_herramienta_browser.md new file mode 100644 index 0000000..9b8a3ac --- /dev/null +++ b/docs/contexto_workspace/idea_y_funcionamiento_herramienta_browser.md @@ -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. diff --git a/docs/manual_de_uso_heramienta.md b/docs/manual_de_uso_heramienta.md new file mode 100644 index 0000000..33c0aa5 --- /dev/null +++ b/docs/manual_de_uso_heramienta.md @@ -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_", + "data": {}, + "state": { + "isOpen": true, + "currentUrl": "https://..." + } +} +``` + +Respuesta con error: + +```json +{ + "ok": false, + "tool": "browser_", + "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 ` + + + + + + + + + diff --git a/scripts/google_es_puertas_rank.mjs b/scripts/google_es_puertas_rank.mjs new file mode 100644 index 0000000..e8224d9 --- /dev/null +++ b/scripts/google_es_puertas_rank.mjs @@ -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) +}) diff --git a/scripts/google_es_v8d.mjs b/scripts/google_es_v8d.mjs new file mode 100644 index 0000000..71da5b1 --- /dev/null +++ b/scripts/google_es_v8d.mjs @@ -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) +}) diff --git a/scripts/google_pagination_v4.mjs b/scripts/google_pagination_v4.mjs new file mode 100644 index 0000000..39aec16 --- /dev/null +++ b/scripts/google_pagination_v4.mjs @@ -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) +}) diff --git a/src/browser/manager.ts b/src/browser/manager.ts new file mode 100644 index 0000000..ee9689b --- /dev/null +++ b/src/browser/manager.ts @@ -0,0 +1,1089 @@ +import { mkdir } from "node:fs/promises" +import path from "node:path" +import { chromium, type Browser, type BrowserContext, type Locator, type Page } from "playwright" + +export type OpenBrowserInput = { + headless?: boolean + startUrl?: string + width?: number + height?: number + recordVideo?: boolean + recordTrace?: boolean + recordLabel?: string + browserKind?: "testing" | "system" + executablePath?: string + userDataDir?: string + persistentProfile?: boolean + verbose?: boolean + verboseOverlay?: boolean + interactionDelayMinMs?: number + interactionDelayMaxMs?: number +} + +export type QueryInput = { + selector: string + mode?: "text" | "html" | "value" | "exists" | "visible" | "enabled" | "count" | "attributes" +} + +type Diag = { + kind: "console" | "pageerror" | "requestfailed" + level?: string + text: string + url?: string + time: number +} + +type StepLog = { + ts: number + action: string + detail?: string +} + +export class BrowserManager { + private browser: Browser | null = null + private context: BrowserContext | null = null + private page: Page | null = null + private artifactsDir: string + private inflight = 0 + private activity = Date.now() + private diag: Diag[] = [] + private recording = { + video: false, + trace: false, + label: "session", + } + private visualCursor = false + private launchProfile = { + browserKind: "testing" as "testing" | "system", + executablePath: null as string | null, + userDataDir: null as string | null, + persistentProfile: false, + } + private verbose = true + private verboseOverlay = true + private interactionDelay = { + minMs: 1000, + maxMs: 3000, + } + private stepLog: StepLog[] = [] + + constructor(artifactsDir: string) { + this.artifactsDir = artifactsDir + } + + async open(input: OpenBrowserInput = {}) { + if (this.browser) { + return { + alreadyOpen: true, + url: this.page?.url() ?? null, + headless: false, + recording: this.recording, + launchProfile: this.launchProfile, + } + } + + const headless = input.headless ?? false + const width = input.width ?? 1440 + const height = input.height ?? 900 + const recordVideo = input.recordVideo ?? false + const recordTrace = input.recordTrace ?? false + const recordLabel = (input.recordLabel ?? "session").replace(/[^a-zA-Z0-9-_]/g, "_") + const browserKind = input.browserKind ?? "testing" + const executablePath = input.executablePath ?? undefined + const persistentProfile = input.persistentProfile ?? false + const userDataDir = input.userDataDir ?? undefined + const verbose = input.verbose ?? true + const verboseOverlay = input.verboseOverlay ?? true + const delayMinMs = this.normalizeDelay(input.interactionDelayMinMs ?? 1000) + const delayMaxMs = this.normalizeDelay(input.interactionDelayMaxMs ?? 3000) + + if (persistentProfile && !userDataDir) { + throw new Error("'userDataDir' is required when 'persistentProfile' is true") + } + + if (recordVideo || recordTrace) { + await mkdir(this.artifactsDir, { recursive: true }) + } + + if (persistentProfile && userDataDir) { + this.context = await chromium.launchPersistentContext(userDataDir, { + headless, + executablePath, + viewport: { width, height }, + recordVideo: recordVideo + ? { + dir: this.artifactsDir, + size: { width, height }, + } + : undefined, + }) + this.browser = this.context.browser() + } else { + this.browser = await chromium.launch({ headless, executablePath }) + this.context = await this.browser.newContext({ + viewport: { width, height }, + recordVideo: recordVideo + ? { + dir: this.artifactsDir, + size: { width, height }, + } + : undefined, + }) + } + + if (!this.context) { + throw new Error("Failed to initialize browser context") + } + this.diag = [] + this.recording = { + video: recordVideo, + trace: recordTrace, + label: recordLabel, + } + this.launchProfile = { + browserKind, + executablePath: executablePath ?? null, + userDataDir: userDataDir ?? null, + persistentProfile, + } + this.verbose = verbose + this.verboseOverlay = verboseOverlay + this.interactionDelay = { + minMs: Math.min(delayMinMs, delayMaxMs), + maxMs: Math.max(delayMinMs, delayMaxMs), + } + this.stepLog = [] + this.visualCursor = !headless + this.track(this.context) + const initialPage = this.context.pages()[0] ?? null + this.page = initialPage ?? (await this.context.newPage()) + if (this.visualCursor) { + await this.ensureVisualCursor(this.page) + await this.ensureVerboseOverlay(this.page) + } + + if (recordTrace) { + await this.context.tracing.start({ screenshots: true, snapshots: true, sources: true }) + } + + if (input.startUrl) { + await this.page.goto(input.startUrl, { waitUntil: "domcontentloaded" }) + if (this.visualCursor) { + await this.ensureVisualCursor(this.page) + await this.ensureVerboseOverlay(this.page) + } + await this.stabilize() + } + + await this.logStep("open", `kind=${browserKind} persistent=${persistentProfile}`) + + return { + alreadyOpen: false, + url: this.page.url(), + headless, + viewport: { width, height }, + recording: this.recording, + launchProfile: this.launchProfile, + behavior: { + verbose: this.verbose, + verboseOverlay: this.verboseOverlay, + interactionDelayMs: this.interactionDelay, + }, + } + } + + async close() { + const wasOpen = Boolean(this.browser) + const page = this.page + const context = this.context + const artifacts: Record = {} + + if (context && this.recording.trace) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const tracePath = path.join(this.artifactsDir, `${timestamp}-${this.recording.label}-trace.zip`) + await context.tracing.stop({ path: tracePath }).catch(() => {}) + artifacts.tracePath = tracePath + } + + if (context) { + await context.close() + } + if (this.recording.video && page?.video()) { + const videoPath = await page.video()?.path().catch(() => null) + if (videoPath) { + artifacts.videoPath = videoPath + } + } + if (this.browser?.isConnected()) { + await this.browser.close().catch(() => {}) + } + + this.context = null + this.browser = null + this.page = null + this.inflight = 0 + this.activity = Date.now() + this.diag = [] + this.recording = { + video: false, + trace: false, + label: "session", + } + this.launchProfile = { + browserKind: "testing", + executablePath: null, + userDataDir: null, + persistentProfile: false, + } + this.verbose = true + this.verboseOverlay = true + this.interactionDelay = { + minMs: 1000, + maxMs: 3000, + } + this.stepLog = [] + this.visualCursor = false + + return { wasOpen, artifacts } + } + + ensurePage(): Page { + if (!this.page) { + throw new Error("Browser session is not open. Call browser_open first.") + } + return this.page + } + + async navigate(url: string, waitUntil: "load" | "domcontentloaded" | "networkidle" = "domcontentloaded") { + const page = this.ensurePage() + await this.beforeStep("navigate", `url=${url}`) + await page.goto(url, { waitUntil }) + if (this.visualCursor) { + await this.ensureVisualCursor(page) + await this.ensureVerboseOverlay(page) + } + const settle = await this.stabilize() + return { url: page.url(), settle } + } + + async click(selector: string, timeoutMs = 10000) { + const page = this.ensurePage() + await this.beforeStep("click", selector, true) + await this.moveMouseToSelector(selector) + await page.click(selector, { timeout: timeoutMs }) + const settle = await this.stabilize() + return { selector, settle } + } + + async hover(selector: string, timeoutMs = 10000) { + const page = this.ensurePage() + await this.beforeStep("hover", selector, true) + await this.moveMouseToSelector(selector) + await page.hover(selector, { timeout: timeoutMs }) + const settle = await this.stabilize() + return { selector, settle } + } + + async type(selector: string, text: string, clear = true, timeoutMs = 10000) { + const page = this.ensurePage() + await this.beforeStep("type", selector, true) + await this.moveMouseToSelector(selector) + await page.click(selector, { timeout: timeoutMs }) + if (clear) { + await page.fill(selector, "", { timeout: timeoutMs }) + } + await page.fill(selector, text, { timeout: timeoutMs }) + const settle = await this.stabilize() + return { selector, length: text.length, settle } + } + + async select(selector: string, value: string, timeoutMs = 10000) { + const page = this.ensurePage() + await this.beforeStep("select", `${selector}=${value}`, true) + const selected = await page.selectOption(selector, value, { timeout: timeoutMs }) + const settle = await this.stabilize() + return { selector, selected, settle } + } + + async press(key: string) { + const page = this.ensurePage() + await this.beforeStep("press", key, true) + await page.keyboard.press(key) + const settle = await this.stabilize() + return { key, settle } + } + + async scroll(input: { x?: number; y?: number; selector?: string }) { + const page = this.ensurePage() + await this.beforeStep("scroll", input.selector ? `selector=${input.selector}` : "page") + const x = input.x ?? 0 + const y = input.y ?? 400 + + if (input.selector) { + await this.moveMouseToSelector(input.selector) + const before = await page.locator(input.selector).evaluate((el) => { + const node = el as HTMLElement + return { scrollTop: node.scrollTop, scrollLeft: node.scrollLeft } + }) + await page.locator(input.selector).evaluate( + (el, payload) => { + const element = el as HTMLElement + element.scrollBy(payload.x, payload.y) + }, + { x, y }, + ) + const after = await page.locator(input.selector).evaluate((el) => { + const node = el as HTMLElement + return { scrollTop: node.scrollTop, scrollLeft: node.scrollLeft } + }) + const settle = await this.stabilize() + return { + selector: input.selector, + x, + y, + movedX: after.scrollLeft - before.scrollLeft, + movedY: after.scrollTop - before.scrollTop, + settle, + } + } + + const start = await page.evaluate(() => { + const doc = document.documentElement + return { + x: window.scrollX, + y: window.scrollY, + maxY: Math.max(0, doc.scrollHeight - window.innerHeight), + } + }) + + if (this.visualCursor) { + const vp = page.viewportSize() + if (vp) { + await this.setVisualCursor(page, Math.round(vp.width * 0.6), Math.round(vp.height * 0.7)) + } + } + + const chunks = Math.max(1, Math.min(8, Math.ceil(Math.abs(y) / 300))) + const stepY = Math.trunc(y / chunks) + for (let i = 0; i < chunks; i++) { + await page.mouse.wheel(x, stepY) + await page.waitForTimeout(80) + } + + let end = await page.evaluate(() => { + const doc = document.documentElement + return { + x: window.scrollX, + y: window.scrollY, + maxY: Math.max(0, doc.scrollHeight - window.innerHeight), + } + }) + + if (end.y === start.y && (x !== 0 || y !== 0)) { + await page.evaluate( + (payload) => { + window.scrollBy(payload.x, payload.y) + }, + { x, y }, + ) + await page.waitForTimeout(80) + end = await page.evaluate(() => { + const doc = document.documentElement + return { + x: window.scrollX, + y: window.scrollY, + maxY: Math.max(0, doc.scrollHeight - window.innerHeight), + } + }) + } + + const settle = await this.stabilize() + return { + x, + y, + chunks, + startY: start.y, + endY: end.y, + maxY: end.maxY, + movedY: end.y - start.y, + settle, + } + } + + async waitFor(input: { + for: "selector" | "text" | "url" | "timeout" + value?: string + timeoutMs?: number + }) { + const page = this.ensurePage() + await this.beforeStep("wait", input.for) + const timeoutMs = input.timeoutMs ?? 10000 + + if (input.for === "timeout") { + const waitMs = input.value ? Number.parseInt(input.value, 10) : timeoutMs + const finalWaitMs = Number.isFinite(waitMs) && waitMs >= 0 ? waitMs : timeoutMs + await page.waitForTimeout(finalWaitMs) + return { waitedMs: finalWaitMs } + } + + if (!input.value) { + throw new Error("wait value is required for selector, text, and url modes") + } + + if (input.for === "selector") { + await page.waitForSelector(input.value, { timeout: timeoutMs }) + return { selector: input.value } + } + + if (input.for === "text") { + await page.getByText(input.value).first().waitFor({ timeout: timeoutMs }) + return { text: input.value } + } + + await page.waitForURL(input.value, { timeout: timeoutMs }) + return { url: page.url() } + } + + async snapshot(input: { label?: string; fullPage?: boolean }) { + const page = this.ensurePage() + await this.beforeStep("snapshot", input.label ?? "snapshot") + await mkdir(this.artifactsDir, { recursive: true }) + + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const label = (input.label ?? "snapshot").replace(/[^a-zA-Z0-9-_]/g, "_") + const fileName = `${timestamp}-${label}.png` + const filePath = path.join(this.artifactsDir, fileName) + + await page.screenshot({ + path: filePath, + fullPage: input.fullPage ?? true, + }) + + return { filePath } + } + + async safeSnapshot(label = "auto-error", fullPage = true) { + try { + if (!this.page) { + return { filePath: null as string | null } + } + const out = await this.snapshot({ label, fullPage }) + return { filePath: out.filePath } + } catch { + return { filePath: null as string | null } + } + } + + async evaluate(script: string, arg: unknown) { + const page = this.ensurePage() + await this.beforeStep("evaluate") + const result = await page.evaluate( + ({ code, value }) => { + // eslint-disable-next-line no-new-func + const run = new Function( + "arg", + `const out = (${code}); if (typeof out === "function") return out(arg); return out;`, + ) as (arg: unknown) => unknown + return run(value) + }, + { code: script, value: arg }, + ) + + const settle = await this.stabilize() + return { result, settle } + } + + async query(input: QueryInput) { + const page = this.ensurePage() + await this.beforeStep("query", `${input.mode ?? "text"}:${input.selector}`) + const mode = input.mode ?? "text" + + if (mode === "count") { + const count = await page.locator(input.selector).count() + return { selector: input.selector, mode, result: count } + } + + if (mode === "exists") { + const exists = (await page.locator(input.selector).count()) > 0 + return { selector: input.selector, mode, result: exists } + } + + const node = page.locator(input.selector).first() + await node.waitFor({ state: "attached", timeout: 10000 }) + + if (mode === "visible") { + const visible = await node.isVisible() + return { selector: input.selector, mode, result: visible } + } + + if (mode === "enabled") { + const enabled = await node.isEnabled() + return { selector: input.selector, mode, result: enabled } + } + + if (mode === "value") { + const value = await node.inputValue().catch(() => null) + return { selector: input.selector, mode, result: value } + } + + if (mode === "html") { + const html = await node.evaluate((el) => (el as HTMLElement).outerHTML) + return { selector: input.selector, mode, result: html } + } + + if (mode === "attributes") { + const attrs = await node.evaluate((el) => { + const map: Record = {} + for (const attr of Array.from(el.attributes)) { + map[attr.name] = attr.value + } + return map + }) + return { selector: input.selector, mode, result: attrs } + } + + const text = await node.innerText() + return { selector: input.selector, mode, result: text } + } + + getState() { + return { + isOpen: Boolean(this.browser), + currentUrl: this.page?.url() ?? null, + } + } + + getHealth() { + return { + isOpen: Boolean(this.browser), + currentUrl: this.page?.url() ?? null, + inflight: this.inflight, + lastActivityAgoMs: Date.now() - this.activity, + recording: this.recording, + launchProfile: this.launchProfile, + behavior: { + verbose: this.verbose, + verboseOverlay: this.verboseOverlay, + interactionDelayMs: this.interactionDelay, + }, + recentSteps: this.stepLog.slice(-20), + } + } + + async observe() { + const page = this.ensurePage() + await this.beforeStep("observe") + const data = await page.evaluate(() => { + const text = (document.body?.innerText || "").toLowerCase() + const title = document.title || "" + const indicators: string[] = [] + + if ( + /cookies|cookie|consent|privacidad|privacy|rechazar todo|aceptar todo|accept all|reject all/.test(text) + ) { + indicators.push("cookie_or_consent_detected") + } + if ( + /comprobar que eres humano|verify you are human|i'm not a robot|captcha|recaptcha|unusual traffic|sorry/.test( + text + " " + title.toLowerCase(), + ) + ) { + indicators.push("human_check_or_captcha_detected") + } + + const heading = document.querySelector("h1,h2,h3")?.textContent?.trim() || null + return { + title, + heading, + indicators, + } + }) + + return { + url: page.url(), + ...data, + } + } + + async handleConsent(action: "reject" | "accept" = "reject") { + const page = this.ensurePage() + await this.beforeStep("handleConsent", action, true) + const labelsReject = [ + "Rechazar todo", + "Rechazar", + "Denegar", + "No aceptar", + "Reject all", + "Reject", + "Decline", + ] + const labelsAccept = ["Aceptar todo", "Aceptar", "Accept all", "Accept", "I agree"] + const labels = action === "reject" ? labelsReject : labelsAccept + + const result = await page.evaluate((opts) => { + const candidates = Array.from( + document.querySelectorAll("button, [role='button'], input[type='button'], input[type='submit']"), + ) as Array + for (const el of candidates) { + const raw = (el as HTMLElement).innerText || (el as HTMLInputElement).value || "" + const txt = raw.trim().toLowerCase() + if (!txt) continue + if (opts.some((k) => txt.includes(k.toLowerCase()))) { + el.click() + return { clicked: true, text: raw.trim() } + } + } + return { clicked: false, text: null } + }, labels) + + const settle = await this.stabilize() + return { ...result, settle, action } + } + + async handleHumanCheck() { + const page = this.ensurePage() + await this.beforeStep("handleHumanCheck", "challenge", true) + const attempts: string[] = [] + + // Try multiple frame selectors and checkbox variants used by reCAPTCHA. + const frameSelectors = [ + "iframe[title*='reCAPTCHA']", + "iframe[title*='recaptcha']", + "iframe[src*='recaptcha']", + "iframe[src*='google.com/recaptcha']", + ] + + const checkboxSelectors = [ + "#recaptcha-anchor", + ".recaptcha-checkbox-border", + "div[role='checkbox']", + "span[role='checkbox']", + "input[type='checkbox']", + "button[aria-label*='robot' i]", + "button[aria-label*='human' i]", + "[id*='captcha' i]", + "[class*='captcha' i]", + ] + + for (const frameSelector of frameSelectors) { + for (const checkboxSelector of checkboxSelectors) { + try { + const target = page.frameLocator(frameSelector).locator(checkboxSelector).first() + const clicked = await this.clickLocatorCenter(target, 1800) + if (!clicked) { + throw new Error("not_clickable") + } + const settle = await this.stabilize() + return { + clicked: true, + method: `frame:${frameSelector}:${checkboxSelector}`, + attempts, + settle, + } + } catch { + attempts.push(`miss:${frameSelector}:${checkboxSelector}`) + } + } + } + + for (const checkboxSelector of checkboxSelectors) { + try { + const target = page.locator(checkboxSelector).first() + const clicked = await this.clickLocatorCenter(target, 1400) + if (!clicked) { + throw new Error("not_clickable") + } + const settle = await this.stabilize() + return { + clicked: true, + method: `page:${checkboxSelector}`, + attempts, + settle, + } + } catch { + attempts.push(`miss:page:${checkboxSelector}`) + } + } + + const coordFallback = await page.evaluate(() => { + const labels = [ + "No soy un robot", + "I'm not a robot", + "Soy humano", + "I am human", + "Verify you are human", + "Comprobar que eres humano", + "Verificar que eres humano", + "Continuar", + "I am not a robot", + "Are you human", + ] + + const nodes = Array.from(document.querySelectorAll("button, [role='button'], input[type='button'], input[type='submit'], label, div, span")) as HTMLElement[] + for (const el of nodes) { + const raw = (el.innerText || (el as HTMLInputElement).value || "").trim() + if (!raw) continue + const txt = raw.toLowerCase() + if (!labels.some((k) => txt.includes(k.toLowerCase()))) continue + + const rect = el.getBoundingClientRect() + if (rect.width < 8 || rect.height < 8) continue + const style = window.getComputedStyle(el) + if (style.visibility === "hidden" || style.display === "none") continue + + return { + clicked: true, + method: "text-coordinates", + text: raw, + x: Math.round(rect.left + rect.width / 2), + y: Math.round(rect.top + rect.height / 2), + } + } + return { clicked: false, method: "text-coordinates", text: null, x: null, y: null } + }) + + if (coordFallback.clicked && coordFallback.x !== null && coordFallback.y !== null) { + await this.clickAt(coordFallback.x, coordFallback.y) + const settle = await this.stabilize() + return { + clicked: true, + method: coordFallback.method, + text: coordFallback.text, + attempts, + settle, + } + } + + // Fallback for visible text-based controls. + const fallback = await page.evaluate(() => { + const labels = [ + "No soy un robot", + "I'm not a robot", + "Soy humano", + "I am human", + "Verify you are human", + "Comprobar que eres humano", + "Verificar que eres humano", + "Continuar", + ] + const candidates = Array.from( + document.querySelectorAll("button, [role='button'], input[type='button'], input[type='submit'], label"), + ) as Array + + for (const el of candidates) { + const raw = (el as HTMLElement).innerText || (el as HTMLInputElement).value || "" + const txt = raw.trim().toLowerCase() + if (!txt) continue + if (labels.some((k) => txt.includes(k.toLowerCase()))) { + el.click() + return { clicked: true, text: raw.trim(), method: "text-fallback" } + } + } + return { clicked: false, text: null, method: "text-fallback" } + }) + + const settle = await this.stabilize() + return { ...fallback, attempts, settle } + } + + getDiagnostics(limit = 20) { + const max = Math.max(1, Math.min(200, Math.floor(limit))) + return { + total: this.diag.length, + events: this.diag.slice(-max), + } + } + + clearDiagnostics() { + const cleared = this.diag.length + this.diag = [] + return { cleared } + } + + private normalizeDelay(ms: number) { + if (!Number.isFinite(ms) || ms < 0) { + return 0 + } + return Math.min(10000, Math.floor(ms)) + } + + private async beforeStep(action: string, detail?: string, humanDelay = false) { + await this.logStep(action, detail) + await this.waitUntilReady() + if (humanDelay) { + await this.humanPause() + } + } + + private async waitUntilReady() { + const page = this.ensurePage() + const idleMs = 450 + const maxMs = 9000 + const stepMs = 120 + const start = Date.now() + + await page.waitForLoadState("domcontentloaded", { timeout: 3000 }).catch(() => {}) + + for (;;) { + const elapsedMs = Date.now() - start + const quietNetwork = this.inflight === 0 && Date.now() - this.activity >= idleMs + let ready = false + try { + const state = await page.evaluate(() => document.readyState) + ready = state === "interactive" || state === "complete" + } catch { + ready = false + } + + if (quietNetwork && ready) { + return { settled: true, elapsedMs, inflight: this.inflight } + } + if (elapsedMs >= maxMs) { + return { settled: false, elapsedMs, inflight: this.inflight, reason: "pre_step_max_wait_reached" } + } + await page.waitForTimeout(stepMs) + } + } + + private async humanPause() { + const page = this.ensurePage() + const minMs = this.interactionDelay.minMs + const maxMs = this.interactionDelay.maxMs + if (maxMs <= 0) return + const delay = minMs >= maxMs ? minMs : Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs + if (delay > 0) { + await this.logStep("human_delay", `${delay}ms`) + await page.waitForTimeout(delay) + } + } + + private async logStep(action: string, detail?: string) { + const entry: StepLog = { + ts: Date.now(), + action, + detail, + } + this.stepLog.push(entry) + if (this.stepLog.length > 500) { + this.stepLog = this.stepLog.slice(-500) + } + + if (!this.verbose) { + return + } + const page = this.page + if (!page || !this.visualCursor || !this.verboseOverlay) { + return + } + const line = detail ? `${action}: ${detail}` : action + await this.setVerboseStatus(page, line) + } + + private track(context: BrowserContext) { + context.on("page", (page) => { + page.on("console", (msg) => { + this.push({ + kind: "console", + level: msg.type(), + text: msg.text(), + url: page.url(), + time: Date.now(), + }) + }) + + page.on("pageerror", (err) => { + this.push({ + kind: "pageerror", + text: err.message, + url: page.url(), + time: Date.now(), + }) + }) + + page.on("requestfailed", (req) => { + const fail = req.failure() + this.push({ + kind: "requestfailed", + text: `${req.method()} ${req.url()}${fail ? ` :: ${fail.errorText}` : ""}`, + url: page.url(), + time: Date.now(), + }) + }) + }) + + context.on("request", () => { + this.inflight += 1 + this.activity = Date.now() + }) + + context.on("requestfinished", () => { + this.inflight = Math.max(0, this.inflight - 1) + this.activity = Date.now() + }) + + context.on("requestfailed", () => { + this.inflight = Math.max(0, this.inflight - 1) + this.activity = Date.now() + }) + } + + private async stabilize() { + const page = this.ensurePage() + const idleMs = 500 + const maxMs = 8000 + const stepMs = 100 + const start = Date.now() + + await page.waitForLoadState("domcontentloaded", { timeout: 3000 }).catch(() => {}) + await page.waitForLoadState("networkidle", { timeout: 2500 }).catch(() => {}) + + for (;;) { + const elapsedMs = Date.now() - start + const quiet = this.inflight === 0 && Date.now() - this.activity >= idleMs + if (quiet) { + return { settled: true, elapsedMs, inflight: this.inflight } + } + if (elapsedMs >= maxMs) { + return { settled: false, elapsedMs, inflight: this.inflight, reason: "max_wait_reached" } + } + await page.waitForTimeout(stepMs) + } + } + + private push(event: Diag) { + this.diag.push(event) + if (this.diag.length > 200) { + this.diag = this.diag.slice(-200) + } + } + + private async moveMouseToSelector(selector: string) { + const page = this.ensurePage() + const locator = page.locator(selector).first() + const box = await locator.boundingBox() + if (!box) { + return { moved: false } + } + const targetX = box.x + box.width / 2 + const targetY = box.y + box.height / 2 + await page.mouse.move(targetX, targetY, { steps: 20 }) + if (this.visualCursor) { + await this.ensureVisualCursor(page) + await this.setVisualCursor(page, Math.round(targetX), Math.round(targetY)) + } + await page.waitForTimeout(60) + return { moved: true, x: Math.round(targetX), y: Math.round(targetY) } + } + + private async clickLocatorCenter(locator: Locator, timeoutMs: number) { + const page = this.ensurePage() + await locator.waitFor({ state: "visible", timeout: timeoutMs }) + const box = await locator.boundingBox() + if (box) { + const x = Math.round(box.x + box.width / 2) + const y = Math.round(box.y + box.height / 2) + await this.clickAt(x, y) + return true + } + await locator.click({ timeout: timeoutMs, force: true }) + return true + } + + private async clickAt(x: number, y: number) { + const page = this.ensurePage() + await page.mouse.move(x, y, { steps: 15 }) + if (this.visualCursor) { + await this.ensureVisualCursor(page) + await this.setVisualCursor(page, x, y) + } + await page.mouse.down() + await page.waitForTimeout(50) + await page.mouse.up() + } + + private async ensureVisualCursor(page: Page) { + await page.evaluate(() => { + const id = "__browser_tool_cursor" + if (document.getElementById(id)) return + const el = document.createElement("div") + el.id = id + el.style.position = "fixed" + el.style.left = "0px" + el.style.top = "0px" + el.style.width = "14px" + el.style.height = "14px" + el.style.borderRadius = "50%" + el.style.background = "rgba(255, 59, 48, 0.95)" + el.style.boxShadow = "0 0 0 2px rgba(255,255,255,0.9), 0 0 10px rgba(255,59,48,0.8)" + el.style.transform = "translate(-50%, -50%)" + el.style.zIndex = "2147483647" + el.style.pointerEvents = "none" + document.documentElement.appendChild(el) + }) + } + + private async ensureVerboseOverlay(page: Page) { + if (!this.verbose || !this.verboseOverlay) return + await page.evaluate(() => { + const id = "__browser_tool_verbose" + if (document.getElementById(id)) return + const el = document.createElement("div") + el.id = id + el.textContent = "browser-tool: ready" + el.style.position = "fixed" + el.style.left = "12px" + el.style.bottom = "12px" + el.style.maxWidth = "55vw" + el.style.padding = "6px 10px" + el.style.borderRadius = "8px" + el.style.background = "rgba(0,0,0,0.68)" + el.style.color = "#ffffff" + el.style.font = "12px/1.4 monospace" + el.style.letterSpacing = "0.2px" + el.style.zIndex = "2147483647" + el.style.pointerEvents = "none" + document.documentElement.appendChild(el) + }) + } + + private async setVerboseStatus(page: Page, text: string) { + await page + .evaluate((payload) => { + const id = "__browser_tool_verbose" + let el = document.getElementById(id) as HTMLElement | null + if (!el) { + el = document.createElement("div") + el.id = id + el.style.position = "fixed" + el.style.left = "12px" + el.style.bottom = "12px" + el.style.maxWidth = "55vw" + el.style.padding = "6px 10px" + el.style.borderRadius = "8px" + el.style.background = "rgba(0,0,0,0.68)" + el.style.color = "#ffffff" + el.style.font = "12px/1.4 monospace" + el.style.letterSpacing = "0.2px" + el.style.zIndex = "2147483647" + el.style.pointerEvents = "none" + document.documentElement.appendChild(el) + } + el.textContent = `browser-tool: ${payload.text}` + }, + { text }, + ) + .catch(() => {}) + } + + private async setVisualCursor(page: Page, x: number, y: number) { + await page.evaluate( + (pos) => { + const el = document.getElementById("__browser_tool_cursor") as HTMLElement | null + if (!el) return + el.style.left = `${pos.x}px` + el.style.top = `${pos.y}px` + }, + { x, y }, + ) + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..5409abe --- /dev/null +++ b/src/server.ts @@ -0,0 +1,1049 @@ +import path from "node:path" +import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises" +import { fileURLToPath } from "node:url" +import { Server } from "@modelcontextprotocol/sdk/server/index.js" +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" +import { + CallToolRequestSchema, + type CallToolResult, + ListToolsRequestSchema, + type ListToolsResult, + type Tool, +} from "@modelcontextprotocol/sdk/types.js" +import { BrowserManager } from "./browser/manager.js" + +const projectInfo = { + name: "opencode-browser-tool", + version: "0.1.0", + transport: "stdio", + browser: "chromium", + mode: "visible-by-default", +} as const + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const artifactsDir = path.resolve(__dirname, "../artifacts") +const configPath = path.resolve(__dirname, "../config/browser-tool.config.json") + +const browser = new BrowserManager(artifactsDir) + +type RuntimeConfig = { + browser: { + defaultKind: "testing" | "system" + systemExecutablePath: string + defaultPersistentProfile: boolean + defaultUserDataDir: string | null + defaultVerbose: boolean + defaultVerboseOverlay: boolean + defaultInteractionDelayMinMs: number + defaultInteractionDelayMaxMs: number + } + report: { + defaultSaveToFile: boolean + defaultFormat: "json" | "markdown" | "both" + } +} + +const defaultConfig: RuntimeConfig = { + browser: { + defaultKind: "testing", + systemExecutablePath: "/usr/bin/google-chrome", + defaultPersistentProfile: false, + defaultUserDataDir: null, + defaultVerbose: true, + defaultVerboseOverlay: true, + defaultInteractionDelayMinMs: 1000, + defaultInteractionDelayMaxMs: 3000, + }, + report: { + defaultSaveToFile: true, + defaultFormat: "both", + }, +} + +let runtimeConfig: RuntimeConfig = { + ...defaultConfig, + browser: { ...defaultConfig.browser }, + report: { ...defaultConfig.report }, +} + +type ReportStep = { + ts: string + tool: string + ok: boolean + durationMs: number + input: Record + data?: unknown + error?: { code?: string; message?: string } +} + +const reportState: { startedAt: string; steps: ReportStep[] } = { + startedAt: new Date().toISOString(), + steps: [], +} + +const tools: Tool[] = [ + { + name: "browser_config", + description: "Get or set persistent browser-tool runtime configuration", + inputSchema: { + type: "object", + properties: { + action: { type: "string", enum: ["get", "set"] }, + browserDefaultKind: { type: "string", enum: ["testing", "system"] }, + browserSystemExecutablePath: { type: "string" }, + browserDefaultPersistentProfile: { type: "boolean" }, + browserDefaultUserDataDir: { type: "string" }, + browserDefaultVerbose: { type: "boolean" }, + browserDefaultVerboseOverlay: { type: "boolean" }, + browserDefaultInteractionDelayMinMs: { type: "number" }, + browserDefaultInteractionDelayMaxMs: { type: "number" }, + reportDefaultSaveToFile: { type: "boolean" }, + reportDefaultFormat: { type: "string", enum: ["json", "markdown", "both"] }, + }, + }, + }, + { + name: "browser_help", + description: "Return browser-tool capabilities, defaults, and usage guide", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "browser_health", + description: "Return runtime health, network activity, and latest artifacts", + inputSchema: { + type: "object", + properties: { + limit: { type: "number" }, + }, + }, + }, + { + name: "browser_diagnostics", + description: "Return recent console, page errors, and failed requests", + inputSchema: { + type: "object", + properties: { + limit: { type: "number" }, + }, + }, + }, + { + name: "browser_diagnostics_clear", + description: "Clear in-memory diagnostics buffer", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "browser_observe", + description: "Capture current page context and optional screenshot for mid-flow diagnosis", + inputSchema: { + type: "object", + properties: { + screenshot: { type: "boolean" }, + label: { type: "string" }, + }, + }, + }, + { + name: "browser_handle_consent", + description: "Try clicking common consent buttons (reject or accept)", + inputSchema: { + type: "object", + properties: { + action: { type: "string", enum: ["reject", "accept"] }, + }, + }, + }, + { + name: "browser_handle_human_check", + description: "Try clicking common human-check controls (including reCAPTCHA checkbox)", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "browser_open", + description: "Open Chromium session in visible mode by default", + inputSchema: { + type: "object", + properties: { + headless: { type: "boolean" }, + startUrl: { type: "string" }, + width: { type: "number" }, + height: { type: "number" }, + browserKind: { type: "string", enum: ["testing", "system"] }, + executablePath: { type: "string" }, + persistentProfile: { type: "boolean" }, + userDataDir: { type: "string" }, + verbose: { type: "boolean" }, + verboseOverlay: { type: "boolean" }, + interactionDelayMinMs: { type: "number" }, + interactionDelayMaxMs: { type: "number" }, + recordVideo: { type: "boolean" }, + recordTrace: { type: "boolean" }, + recordLabel: { type: "string" }, + }, + }, + }, + { + name: "browser_close", + description: "Close current browser session", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "browser_navigate", + description: "Navigate the active page to URL", + inputSchema: { + type: "object", + required: ["url"], + properties: { + url: { type: "string" }, + waitUntil: { type: "string", enum: ["load", "domcontentloaded", "networkidle"] }, + }, + }, + }, + { + name: "browser_click", + description: "Click an element by selector", + inputSchema: { + type: "object", + required: ["selector"], + properties: { + selector: { type: "string" }, + timeoutMs: { type: "number" }, + }, + }, + }, + { + name: "browser_hover", + description: "Hover over an element by selector", + inputSchema: { + type: "object", + required: ["selector"], + properties: { + selector: { type: "string" }, + timeoutMs: { type: "number" }, + }, + }, + }, + { + name: "browser_type", + description: "Type text into an element selector", + inputSchema: { + type: "object", + required: ["selector", "text"], + properties: { + selector: { type: "string" }, + text: { type: "string" }, + clear: { type: "boolean" }, + timeoutMs: { type: "number" }, + }, + }, + }, + { + name: "browser_select", + description: "Select option value in a select element", + inputSchema: { + type: "object", + required: ["selector", "value"], + properties: { + selector: { type: "string" }, + value: { type: "string" }, + timeoutMs: { type: "number" }, + }, + }, + }, + { + name: "browser_press", + description: "Press keyboard key on active page", + inputSchema: { + type: "object", + required: ["key"], + properties: { + key: { type: "string" }, + }, + }, + }, + { + name: "browser_scroll", + description: "Scroll the page or an element", + inputSchema: { + type: "object", + properties: { + x: { type: "number" }, + y: { type: "number" }, + selector: { type: "string" }, + }, + }, + }, + { + name: "browser_wait", + description: "Wait by selector, text, url or timeout", + inputSchema: { + type: "object", + required: ["for"], + properties: { + for: { type: "string", enum: ["selector", "text", "url", "timeout"] }, + value: { type: "string" }, + timeoutMs: { type: "number" }, + }, + }, + }, + { + name: "browser_snapshot", + description: "Take screenshot and store artifact", + inputSchema: { + type: "object", + properties: { + label: { type: "string" }, + fullPage: { type: "boolean" }, + }, + }, + }, + { + name: "browser_evaluate", + description: "Evaluate JavaScript function source on page with optional arg", + inputSchema: { + type: "object", + required: ["script"], + properties: { + script: { type: "string" }, + arg: { type: "object" }, + }, + }, + }, + { + name: "browser_query", + description: "Read DOM/UI state by selector (text, value, exists, visible, enabled, count, attributes, html)", + inputSchema: { + type: "object", + required: ["selector"], + properties: { + selector: { type: "string" }, + mode: { + type: "string", + enum: ["text", "html", "value", "exists", "visible", "enabled", "count", "attributes"], + }, + }, + }, + }, + { + name: "browser_report", + description: "Build consolidated execution report (json/markdown) from steps, diagnostics, and artifacts", + inputSchema: { + type: "object", + properties: { + format: { type: "string", enum: ["json", "markdown", "both"] }, + includeSteps: { type: "boolean" }, + includeDiagnostics: { type: "boolean" }, + includeArtifacts: { type: "boolean" }, + saveToFile: { type: "boolean" }, + label: { type: "string" }, + limitSteps: { type: "number" }, + limitDiagnostics: { type: "number" }, + }, + }, + }, +] + +function asObject(input: unknown): Record { + if (!input || typeof input !== "object" || Array.isArray(input)) { + return {} + } + return input as Record +} + +function getString(value: unknown, field: string): string { + if (typeof value !== "string" || value.length === 0) { + throw new Error(`'${field}' must be a non-empty string`) + } + return value +} + +function getNumber(value: unknown, field: string): number | undefined { + if (value === undefined) { + return undefined + } + if (typeof value !== "number" || Number.isNaN(value)) { + throw new Error(`'${field}' must be a number`) + } + return value +} + +function getBoolean(value: unknown, field: string): boolean | undefined { + if (value === undefined) { + return undefined + } + if (typeof value !== "boolean") { + throw new Error(`'${field}' must be a boolean`) + } + return value +} + +function result(payload: unknown): CallToolResult { + return { + content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], + } +} + +function ok(tool: string, data: unknown, extra?: Record) { + return result({ + ok: true, + tool, + data, + state: browser.getState(), + ...(extra ?? {}), + }) +} + +async function loadConfig() { + try { + const raw = await readFile(configPath, "utf8") + const parsed = JSON.parse(raw) as Partial + runtimeConfig = { + browser: { + defaultKind: parsed?.browser?.defaultKind ?? defaultConfig.browser.defaultKind, + systemExecutablePath: parsed?.browser?.systemExecutablePath ?? defaultConfig.browser.systemExecutablePath, + defaultPersistentProfile: + parsed?.browser?.defaultPersistentProfile ?? defaultConfig.browser.defaultPersistentProfile, + defaultUserDataDir: parsed?.browser?.defaultUserDataDir ?? defaultConfig.browser.defaultUserDataDir, + defaultVerbose: parsed?.browser?.defaultVerbose ?? defaultConfig.browser.defaultVerbose, + defaultVerboseOverlay: parsed?.browser?.defaultVerboseOverlay ?? defaultConfig.browser.defaultVerboseOverlay, + defaultInteractionDelayMinMs: + parsed?.browser?.defaultInteractionDelayMinMs ?? defaultConfig.browser.defaultInteractionDelayMinMs, + defaultInteractionDelayMaxMs: + parsed?.browser?.defaultInteractionDelayMaxMs ?? defaultConfig.browser.defaultInteractionDelayMaxMs, + }, + report: { + defaultSaveToFile: parsed?.report?.defaultSaveToFile ?? defaultConfig.report.defaultSaveToFile, + defaultFormat: parsed?.report?.defaultFormat ?? defaultConfig.report.defaultFormat, + }, + } + } catch { + runtimeConfig = { + ...defaultConfig, + browser: { ...defaultConfig.browser }, + report: { ...defaultConfig.report }, + } + } +} + +async function persistConfig() { + await mkdir(path.dirname(configPath), { recursive: true }) + await writeFile(configPath, JSON.stringify(runtimeConfig, null, 2), "utf8") +} + +function resetReportState() { + reportState.startedAt = new Date().toISOString() + reportState.steps = [] +} + +function parsePayload(response: CallToolResult): Record | null { + const content = response.content?.[0] + if (!content || content.type !== "text") { + return null + } + try { + const parsed = JSON.parse(content.text) as unknown + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record + } + return null + } catch { + return null + } +} + +function recordStep(tool: string, input: Record, response: CallToolResult, durationMs: number) { + const payload = parsePayload(response) + const entry: ReportStep = { + ts: new Date().toISOString(), + tool, + ok: payload?.ok === true, + durationMs, + input, + data: payload?.data, + error: + payload?.error && typeof payload.error === "object" + ? { + code: String((payload.error as Record).code ?? ""), + message: String((payload.error as Record).message ?? ""), + } + : undefined, + } + reportState.steps.push(entry) + if (reportState.steps.length > 1000) { + reportState.steps = reportState.steps.slice(-1000) + } +} + +function markdownReport(input: { + summary: Record + steps?: ReportStep[] + diagnostics?: ReturnType + artifacts?: Array<{ path: string; mtimeMs: number; size: number }> +}) { + const lines: string[] = [] + lines.push("## Browser Report") + lines.push("") + lines.push(`- generatedAt: ${String(input.summary.generatedAt)}`) + lines.push(`- startedAt: ${String(input.summary.startedAt)}`) + lines.push(`- totalSteps: ${String(input.summary.totalSteps)}`) + lines.push(`- failedSteps: ${String(input.summary.failedSteps)}`) + lines.push(`- currentUrl: ${String(input.summary.currentUrl)}`) + lines.push("") + + if (input.steps) { + lines.push("## Steps") + for (const step of input.steps) { + lines.push( + `- [${step.ok ? "OK" : "ERR"}] ${step.tool} (${step.durationMs}ms) input=${JSON.stringify(step.input)}`, + ) + if (step.error?.message) { + lines.push(` error: ${step.error.message}`) + } + } + lines.push("") + } + + if (input.diagnostics) { + lines.push("## Diagnostics") + lines.push(`- total: ${input.diagnostics.total}`) + for (const ev of input.diagnostics.events) { + lines.push(`- ${ev.kind}${ev.level ? `/${ev.level}` : ""}: ${ev.text}`) + } + lines.push("") + } + + if (input.artifacts) { + lines.push("## Artifacts") + for (const art of input.artifacts) { + lines.push(`- ${art.path} (${art.size} bytes)`) + } + lines.push("") + } + + return lines.join("\n") +} + +async function recent(dir: string, limit = 5) { + const max = Math.max(1, Math.min(20, Math.floor(limit))) + const names = await readdir(dir).catch(() => []) + const files = await Promise.all( + names.map(async (name) => { + const file = path.join(dir, name) + const info = await stat(file).catch(() => null) + if (!info || !info.isFile()) return null + return { + path: file, + mtimeMs: info.mtimeMs, + size: info.size, + } + }), + ) + + return files + .filter((x): x is { path: string; mtimeMs: number; size: number } => Boolean(x)) + .sort((a, b) => b.mtimeMs - a.mtimeMs) + .slice(0, max) +} + +function errorResult(tool: string, error: unknown, extra?: Record): CallToolResult { + const message = error instanceof Error ? error.message : String(error) + const code = message.includes("Browser session is not open") ? "SESSION_NOT_OPEN" : "TOOL_EXECUTION_ERROR" + return { + isError: true, + content: [ + { + type: "text", + text: JSON.stringify( + { + ok: false, + tool, + error: { + code, + message, + }, + state: browser.getState(), + ...(extra ?? {}), + }, + null, + 2, + ), + }, + ], + } +} + +async function startServer() { + await loadConfig() + + const server = new Server( + { + name: projectInfo.name, + version: projectInfo.version, + }, + { + capabilities: { + tools: {}, + }, + }, + ) + + server.setRequestHandler(ListToolsRequestSchema, async (): Promise => { + return { tools: [...tools] } + }) + + server.setRequestHandler(CallToolRequestSchema, async ({ params }): Promise => { + const args = asObject(params.arguments) + const started = Date.now() + let response: CallToolResult + + try { + switch (params.name) { + case "browser_help": { + response = ok( + params.name, + { + project: projectInfo, + responseSchema: { + success: { + ok: true, + tool: "", + data: "", + state: "", + }, + error: { + ok: false, + tool: "", + error: { code: "", message: "" }, + state: "", + }, + }, + defaults: { + mode: "visible", + viewport: { width: 1440, height: 900 }, + timeoutMs: 10000, + recording: { recordVideo: false, recordTrace: false }, + browser: { + defaultKind: runtimeConfig.browser.defaultKind, + systemExecutablePath: runtimeConfig.browser.systemExecutablePath, + defaultPersistentProfile: runtimeConfig.browser.defaultPersistentProfile, + defaultUserDataDir: runtimeConfig.browser.defaultUserDataDir, + defaultVerbose: runtimeConfig.browser.defaultVerbose, + defaultVerboseOverlay: runtimeConfig.browser.defaultVerboseOverlay, + defaultInteractionDelayMinMs: runtimeConfig.browser.defaultInteractionDelayMinMs, + defaultInteractionDelayMaxMs: runtimeConfig.browser.defaultInteractionDelayMaxMs, + }, + waitBehavior: + "pre-step page readiness guard + automatic stabilization after navigate/click/type/press/scroll/evaluate", + }, + capabilities: { + session: ["browser_open", "browser_close"], + navigation: ["browser_navigate", "browser_wait"], + interaction: [ + "browser_click", + "browser_hover", + "browser_type", + "browser_select", + "browser_press", + "browser_scroll", + ], + inspection: ["browser_evaluate"], + query: ["browser_query"], + evidence: ["browser_snapshot"], + diagnostics: ["browser_diagnostics", "browser_diagnostics_clear", "browser_health"], + reporting: ["browser_report"], + config: ["browser_config"], + recovery: ["browser_observe", "browser_handle_consent", "browser_handle_human_check"], + }, + notes: [ + "All tools except browser_open and browser_close require an open session.", + "browser_open supports browserKind=testing (Playwright managed) and browserKind=system (system Chrome path).", + "Persistent user profile is available via persistentProfile=true + userDataDir.", + "Verbose mode is enabled by default (overlay visible in headed mode) and can be disabled per run.", + "Interaction delay defaults to 1-3 seconds for click-like actions and is configurable.", + "browser_wait is optional for common flows because native auto-wait is enabled.", + "browser_report defaults to saveToFile=true unless overridden per call.", + ], + examples: [ + "Open and navigate: browser_open -> browser_navigate", + "Use system browser profile: browser_open(browserKind=system, persistentProfile=true, userDataDir=/home/user/.chrome-profile)", + "Login flow: browser_type(email) -> browser_type(password) -> browser_click(submit)", + "Form flow with select: browser_select -> browser_click(next)", + "Read field value: browser_query(selector=#email, mode=value)", + "Debug runtime issues: browser_diagnostics(limit=20)", + "Mid-flow check: browser_observe(screenshot=true)", + "Consent fast-path: browser_handle_consent(action=reject)", + "Human-check fast-path: browser_handle_human_check()", + "Disable verbose overlay: browser_open(verbose=false) or browser_open(verboseOverlay=false)", + "Tune human delay: browser_open(interactionDelayMinMs=1000, interactionDelayMaxMs=3000)", + "Record a run: browser_open(recordVideo=true, recordTrace=true, recordLabel=case123)", + "No video by default: browser_open() uses recordVideo=false unless requested", + "Temporary override: browser_report(saveToFile=false) for current call only", + "Persistent change: browser_config(action=set, reportDefaultSaveToFile=false)", + "Evidence: browser_snapshot with a descriptive label", + ], + config: { + path: configPath, + current: runtimeConfig, + }, + }, + { onboarding: "Run browser_help then browser_health at session start" }, + ) + break + } + case "browser_config": { + const action = (args.action as "get" | "set" | undefined) ?? "get" + if (action === "set") { + const browserKind = args.browserDefaultKind as "testing" | "system" | undefined + const systemExecutablePath = + args.browserSystemExecutablePath !== undefined + ? getString(args.browserSystemExecutablePath, "browserSystemExecutablePath") + : undefined + const defaultPersistentProfile = getBoolean( + args.browserDefaultPersistentProfile, + "browserDefaultPersistentProfile", + ) + const defaultUserDataDir = + args.browserDefaultUserDataDir !== undefined + ? getString(args.browserDefaultUserDataDir, "browserDefaultUserDataDir") + : undefined + const defaultVerbose = getBoolean(args.browserDefaultVerbose, "browserDefaultVerbose") + const defaultVerboseOverlay = getBoolean(args.browserDefaultVerboseOverlay, "browserDefaultVerboseOverlay") + const defaultInteractionDelayMinMs = getNumber( + args.browserDefaultInteractionDelayMinMs, + "browserDefaultInteractionDelayMinMs", + ) + const defaultInteractionDelayMaxMs = getNumber( + args.browserDefaultInteractionDelayMaxMs, + "browserDefaultInteractionDelayMaxMs", + ) + const save = getBoolean(args.reportDefaultSaveToFile, "reportDefaultSaveToFile") + const fmt = args.reportDefaultFormat as "json" | "markdown" | "both" | undefined + if (browserKind) { + runtimeConfig.browser.defaultKind = browserKind + } + if (systemExecutablePath !== undefined) { + runtimeConfig.browser.systemExecutablePath = systemExecutablePath + } + if (defaultPersistentProfile !== undefined) { + runtimeConfig.browser.defaultPersistentProfile = defaultPersistentProfile + } + if (defaultUserDataDir !== undefined) { + runtimeConfig.browser.defaultUserDataDir = defaultUserDataDir + } + if (defaultVerbose !== undefined) { + runtimeConfig.browser.defaultVerbose = defaultVerbose + } + if (defaultVerboseOverlay !== undefined) { + runtimeConfig.browser.defaultVerboseOverlay = defaultVerboseOverlay + } + if (defaultInteractionDelayMinMs !== undefined) { + runtimeConfig.browser.defaultInteractionDelayMinMs = defaultInteractionDelayMinMs + } + if (defaultInteractionDelayMaxMs !== undefined) { + runtimeConfig.browser.defaultInteractionDelayMaxMs = defaultInteractionDelayMaxMs + } + if (save !== undefined) { + runtimeConfig.report.defaultSaveToFile = save + } + if (fmt) { + runtimeConfig.report.defaultFormat = fmt + } + await persistConfig() + response = ok(params.name, { action, config: runtimeConfig, configPath }) + break + } + response = ok(params.name, { action: "get", config: runtimeConfig, configPath }) + break + } + case "browser_health": { + const limit = getNumber(args.limit, "limit") ?? 5 + const artifacts = await recent(artifactsDir, limit) + response = ok(params.name, { + health: browser.getHealth(), + artifacts, + }) + break + } + case "browser_diagnostics": { + const limit = getNumber(args.limit, "limit") ?? 20 + response = ok(params.name, browser.getDiagnostics(limit)) + break + } + case "browser_diagnostics_clear": { + response = ok(params.name, browser.clearDiagnostics()) + break + } + case "browser_observe": { + const screenshot = getBoolean(args.screenshot, "screenshot") ?? true + const label = args.label ? getString(args.label, "label") : `observe-${params.name}` + const observation = await browser.observe() + const shot = screenshot ? await browser.safeSnapshot(label, true) : { filePath: null } + response = ok(params.name, { observation, screenshot: shot.filePath }) + break + } + case "browser_handle_consent": { + const action = (args.action as "reject" | "accept" | undefined) ?? "reject" + const out = await browser.handleConsent(action) + response = ok(params.name, out) + break + } + case "browser_handle_human_check": { + const out = await browser.handleHumanCheck() + response = ok(params.name, out) + break + } + case "browser_open": { + const browserKind = (args.browserKind as "testing" | "system" | undefined) ?? runtimeConfig.browser.defaultKind + const executablePathInput = args.executablePath ? getString(args.executablePath, "executablePath") : undefined + const executablePath = + executablePathInput ?? (browserKind === "system" ? runtimeConfig.browser.systemExecutablePath : undefined) + const userDataDirInput = args.userDataDir ? getString(args.userDataDir, "userDataDir") : undefined + const userDataDir = userDataDirInput ?? runtimeConfig.browser.defaultUserDataDir ?? undefined + const persistentArg = getBoolean(args.persistentProfile, "persistentProfile") + const persistentProfile = persistentArg ?? (userDataDir ? true : runtimeConfig.browser.defaultPersistentProfile) + const verbose = getBoolean(args.verbose, "verbose") ?? runtimeConfig.browser.defaultVerbose + const verboseOverlay = + getBoolean(args.verboseOverlay, "verboseOverlay") ?? runtimeConfig.browser.defaultVerboseOverlay + const interactionDelayMinMs = + getNumber(args.interactionDelayMinMs, "interactionDelayMinMs") ?? + runtimeConfig.browser.defaultInteractionDelayMinMs + const interactionDelayMaxMs = + getNumber(args.interactionDelayMaxMs, "interactionDelayMaxMs") ?? + runtimeConfig.browser.defaultInteractionDelayMaxMs + + const out = await browser.open({ + headless: getBoolean(args.headless, "headless"), + startUrl: args.startUrl ? getString(args.startUrl, "startUrl") : undefined, + width: getNumber(args.width, "width"), + height: getNumber(args.height, "height"), + browserKind, + executablePath, + persistentProfile, + userDataDir, + verbose, + verboseOverlay, + interactionDelayMinMs, + interactionDelayMaxMs, + recordVideo: getBoolean(args.recordVideo, "recordVideo"), + recordTrace: getBoolean(args.recordTrace, "recordTrace"), + recordLabel: args.recordLabel ? getString(args.recordLabel, "recordLabel") : undefined, + }) + if (!out.alreadyOpen) { + resetReportState() + } + response = ok(params.name, out) + break + } + case "browser_close": { + const out = await browser.close() + response = ok(params.name, out) + break + } + case "browser_navigate": { + const url = getString(args.url, "url") + const waitUntil = (args.waitUntil as "load" | "domcontentloaded" | "networkidle" | undefined) ?? "domcontentloaded" + const out = await browser.navigate(url, waitUntil) + response = ok(params.name, out) + break + } + case "browser_click": { + const selector = getString(args.selector, "selector") + const timeoutMs = getNumber(args.timeoutMs, "timeoutMs") ?? 10000 + const out = await browser.click(selector, timeoutMs) + response = ok(params.name, out) + break + } + case "browser_hover": { + const selector = getString(args.selector, "selector") + const timeoutMs = getNumber(args.timeoutMs, "timeoutMs") ?? 10000 + const out = await browser.hover(selector, timeoutMs) + response = ok(params.name, out) + break + } + case "browser_type": { + const selector = getString(args.selector, "selector") + const text = getString(args.text, "text") + const clear = getBoolean(args.clear, "clear") ?? true + const timeoutMs = getNumber(args.timeoutMs, "timeoutMs") ?? 10000 + const out = await browser.type(selector, text, clear, timeoutMs) + response = ok(params.name, out) + break + } + case "browser_select": { + const selector = getString(args.selector, "selector") + const value = getString(args.value, "value") + const timeoutMs = getNumber(args.timeoutMs, "timeoutMs") ?? 10000 + const out = await browser.select(selector, value, timeoutMs) + response = ok(params.name, out) + break + } + case "browser_press": { + const key = getString(args.key, "key") + const out = await browser.press(key) + response = ok(params.name, out) + break + } + case "browser_scroll": { + const out = await browser.scroll({ + selector: args.selector ? getString(args.selector, "selector") : undefined, + x: getNumber(args.x, "x"), + y: getNumber(args.y, "y"), + }) + response = ok(params.name, out) + break + } + case "browser_wait": { + const waitFor = getString(args.for, "for") as "selector" | "text" | "url" | "timeout" + const timeoutMs = getNumber(args.timeoutMs, "timeoutMs") + const out = await browser.waitFor({ + for: waitFor, + value: args.value ? getString(args.value, "value") : undefined, + timeoutMs, + }) + response = ok(params.name, out) + break + } + case "browser_snapshot": { + const out = await browser.snapshot({ + label: args.label ? getString(args.label, "label") : undefined, + fullPage: getBoolean(args.fullPage, "fullPage"), + }) + response = ok(params.name, out) + break + } + case "browser_evaluate": { + const script = getString(args.script, "script") + const out = await browser.evaluate(script, args.arg) + response = ok(params.name, out) + break + } + case "browser_query": { + const selector = getString(args.selector, "selector") + const mode = + (args.mode as + | "text" + | "html" + | "value" + | "exists" + | "visible" + | "enabled" + | "count" + | "attributes" + | undefined) ?? "text" + const out = await browser.query({ selector, mode }) + response = ok(params.name, out) + break + } + case "browser_report": { + const format = (args.format as "json" | "markdown" | "both" | undefined) ?? runtimeConfig.report.defaultFormat + const includeSteps = getBoolean(args.includeSteps, "includeSteps") ?? true + const includeDiagnostics = getBoolean(args.includeDiagnostics, "includeDiagnostics") ?? true + const includeArtifacts = getBoolean(args.includeArtifacts, "includeArtifacts") ?? true + const saveToFile = getBoolean(args.saveToFile, "saveToFile") ?? runtimeConfig.report.defaultSaveToFile + const label = (args.label ? getString(args.label, "label") : "report").replace(/[^a-zA-Z0-9-_]/g, "_") + const limitSteps = getNumber(args.limitSteps, "limitSteps") ?? 200 + const limitDiagnostics = getNumber(args.limitDiagnostics, "limitDiagnostics") ?? 50 + + const steps = includeSteps ? reportState.steps.slice(-Math.max(1, Math.floor(limitSteps))) : undefined + const diagnostics = includeDiagnostics ? browser.getDiagnostics(limitDiagnostics) : undefined + const artifacts = includeArtifacts ? await recent(artifactsDir, 20) : undefined + const summary = { + generatedAt: new Date().toISOString(), + startedAt: reportState.startedAt, + totalSteps: reportState.steps.length, + failedSteps: reportState.steps.filter((s) => !s.ok).length, + currentUrl: browser.getState().currentUrl, + } + + const reportJson = { + summary, + steps, + diagnostics, + artifacts, + health: browser.getHealth(), + } + const reportMd = markdownReport({ summary, steps, diagnostics, artifacts }) + + let saved: Record = {} + if (saveToFile) { + await mkdir(artifactsDir, { recursive: true }) + const stamp = new Date().toISOString().replace(/[:.]/g, "-") + if (format === "json" || format === "both") { + const jsonPath = path.join(artifactsDir, `${stamp}-${label}-report.json`) + await writeFile(jsonPath, JSON.stringify(reportJson, null, 2), "utf8") + saved = { ...saved, jsonPath } + } + if (format === "markdown" || format === "both") { + const mdPath = path.join(artifactsDir, `${stamp}-${label}-report.md`) + await writeFile(mdPath, reportMd, "utf8") + saved = { ...saved, markdownPath: mdPath } + } + } + + response = + format === "json" + ? ok(params.name, { format, saveToFile, report: reportJson, saved, defaults: runtimeConfig.report }) + : format === "markdown" + ? ok(params.name, { format, saveToFile, report: reportMd, saved, defaults: runtimeConfig.report }) + : ok(params.name, { + format, + saveToFile, + report: { json: reportJson, markdown: reportMd }, + saved, + defaults: runtimeConfig.report, + }) + break + } + default: + throw new Error(`Unknown tool '${params.name}'`) + } + } catch (error) { + let extra: Record | undefined + if (browser.getState().isOpen) { + const shot = await browser.safeSnapshot(`error-${params.name}`, true) + const obs = await browser.observe().catch(() => null) + extra = { + autoSnapshot: shot.filePath, + observation: obs, + } + } + response = errorResult(params.name, error, extra) + } + + const durationMs = Date.now() - started + recordStep(params.name, args, response, durationMs) + return response + }) + + process.on("SIGINT", async () => { + await browser.close() + process.exit(0) + }) + + process.on("SIGTERM", async () => { + await browser.close() + process.exit(0) + }) + + const transport = new StdioServerTransport() + await server.connect(transport) +} + +void startServer().catch((error) => { + const message = error instanceof Error ? error.stack ?? error.message : String(error) + process.stderr.write(`Failed to start MCP server: ${message}\n`) + process.exit(1) +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b51d36f --- /dev/null +++ b/tsconfig.json @@ -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"] +}