zerochat 9.6.0__tar.gz → 9.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of zerochat might be problematic. Click here for more details.
- {zerochat-9.6.0/zerochat.egg-info → zerochat-9.8.0}/PKG-INFO +3 -3
- {zerochat-9.6.0 → zerochat-9.8.0}/README.md +2 -2
- {zerochat-9.6.0 → zerochat-9.8.0}/pyproject.toml +1 -1
- {zerochat-9.6.0 → zerochat-9.8.0/zerochat.egg-info}/PKG-INFO +3 -3
- {zerochat-9.6.0 → zerochat-9.8.0}/zerochat.py +441 -67
- {zerochat-9.6.0 → zerochat-9.8.0}/LICENSE +0 -0
- {zerochat-9.6.0 → zerochat-9.8.0}/setup.cfg +0 -0
- {zerochat-9.6.0 → zerochat-9.8.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-9.6.0 → zerochat-9.8.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-9.6.0 → zerochat-9.8.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-9.6.0 → zerochat-9.8.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zerochat
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.8.0
|
|
4
4
|
Summary: Private local web client for AI models, tools and MCP services
|
|
5
5
|
Author: Alberto Alday
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,7 +43,7 @@ The interface runs entirely in the browser as static files: history and settings
|
|
|
43
43
|
|
|
44
44
|
### Quick start
|
|
45
45
|
|
|
46
|
-
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a
|
|
46
|
+
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a profile, and start chatting.
|
|
47
47
|
2. **With local computer tools (Python backend):**
|
|
48
48
|
|
|
49
49
|
```bash
|
|
@@ -93,7 +93,7 @@ La interfaz se ejecuta íntegramente en el navegador como archivos estáticos: e
|
|
|
93
93
|
|
|
94
94
|
### Inicio rápido
|
|
95
95
|
|
|
96
|
-
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil
|
|
96
|
+
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil y comienza.
|
|
97
97
|
2. **Con herramientas del equipo (backend Python):**
|
|
98
98
|
|
|
99
99
|
```bash
|
|
@@ -21,7 +21,7 @@ The interface runs entirely in the browser as static files: history and settings
|
|
|
21
21
|
|
|
22
22
|
### Quick start
|
|
23
23
|
|
|
24
|
-
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a
|
|
24
|
+
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a profile, and start chatting.
|
|
25
25
|
2. **With local computer tools (Python backend):**
|
|
26
26
|
|
|
27
27
|
```bash
|
|
@@ -71,7 +71,7 @@ La interfaz se ejecuta íntegramente en el navegador como archivos estáticos: e
|
|
|
71
71
|
|
|
72
72
|
### Inicio rápido
|
|
73
73
|
|
|
74
|
-
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil
|
|
74
|
+
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil y comienza.
|
|
75
75
|
2. **Con herramientas del equipo (backend Python):**
|
|
76
76
|
|
|
77
77
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zerochat
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.8.0
|
|
4
4
|
Summary: Private local web client for AI models, tools and MCP services
|
|
5
5
|
Author: Alberto Alday
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,7 +43,7 @@ The interface runs entirely in the browser as static files: history and settings
|
|
|
43
43
|
|
|
44
44
|
### Quick start
|
|
45
45
|
|
|
46
|
-
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a
|
|
46
|
+
1. **Web (client-only):** Open [ZeroChat on GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configure a profile, and start chatting.
|
|
47
47
|
2. **With local computer tools (Python backend):**
|
|
48
48
|
|
|
49
49
|
```bash
|
|
@@ -93,7 +93,7 @@ La interfaz se ejecuta íntegramente en el navegador como archivos estáticos: e
|
|
|
93
93
|
|
|
94
94
|
### Inicio rápido
|
|
95
95
|
|
|
96
|
-
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil
|
|
96
|
+
1. **Web directa (solo cliente):** Abre [ZeroChat en GitHub Pages](https://albalday.github.io/zerochat/zerochat.html), configura un perfil y comienza.
|
|
97
97
|
2. **Con herramientas del equipo (backend Python):**
|
|
98
98
|
|
|
99
99
|
```bash
|
|
@@ -46,7 +46,7 @@ import zlib
|
|
|
46
46
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
47
47
|
from pathlib import Path
|
|
48
48
|
|
|
49
|
-
SOURCE_BACKEND_VERSION = "9.
|
|
49
|
+
SOURCE_BACKEND_VERSION = "9.8.0"
|
|
50
50
|
|
|
51
51
|
def _read_source_version(filename: str) -> str | None:
|
|
52
52
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -1101,7 +1101,7 @@ def job_details(job: CommandJob) -> dict:
|
|
|
1101
1101
|
# Generated from services/ by scripts/build-managed-services.mjs. Do not edit.
|
|
1102
1102
|
# ==============================================================================
|
|
1103
1103
|
|
|
1104
|
-
MANAGED_SERVICE_FILES: dict[str, str] = json.loads("{\n \"composio/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"mcp-remote\\\",\\n \\\"version\\\": \\\"0.13.5\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"composio/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"composio\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"MCP remoto para acceder a aplicaciones como Gmail, Google Drive, Slack o GitHub. Composio solicita la autorización de cada aplicación cuando sea necesaria.\\\",\\n \\\"en\\\": \\\"Remote MCP for apps such as Gmail, Google Drive, Slack, and GitHub. Composio requests authorization for each app when it is needed.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"https://docs.composio.dev/docs/composio-connect\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía oficial de Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect official guide\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"remote\\\": {\\n \\\"type\\\": \\\"mcp-remote\\\",\\n \\\"transport\\\": \\\"streamable-http\\\",\\n \\\"url\\\": \\\"https://connect.composio.dev/mcp\\\",\\n \\\"authentication\\\": \\\"composio-connect\\\"\\n },\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\n \\\"${serviceDir}/node_modules/mcp-remote/dist/proxy.js\\\",\\n \\\"https://connect.composio.dev/mcp\\\",\\n \\\"--protocol\\\",\\n \\\"legacy\\\",\\n \\\"--static-oauth-client-metadata\\\",\\n \\\"{\\\\\\\"client_name\\\\\\\":\\\\\\\"zerochat-mcp-remote\\\\\\\"}\\\",\\n \\\"--auth-timeout\\\",\\n \\\"120\\\"\\n ],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"HOME\\\": \\\"${serviceDir}\\\",\\n \\\"USERPROFILE\\\": \\\"${serviceDir}\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 150\\n }\\n}\\n\",\n \"ejemplo/ejemplo_mcp_server.py\": \"#!/usr/bin/env python3\\nimport json\\nimport sys\\n\\ndef reply(req_id, result=None, error=None):\\n resp = {\\\"jsonrpc\\\": \\\"2.0\\\", \\\"id\\\": req_id}\\n if error:\\n resp[\\\"error\\\"] = error\\n else:\\n resp[\\\"result\\\"] = result\\n sys.stdout.write(json.dumps(resp) + \\\"\\\\n\\\")\\n sys.stdout.flush()\\n\\nfor raw in sys.stdin:\\n try:\\n req = json.loads(raw)\\n except Exception:\\n continue\\n req_id = req.get(\\\"id\\\")\\n method = req.get(\\\"method\\\")\\n params = req.get(\\\"params\\\", {})\\n if method == \\\"initialize\\\":\\n reply(req_id, {\\n \\\"protocolVersion\\\": \\\"2024-11-05\\\",\\n \\\"serverInfo\\\": {\\\"name\\\": \\\"ZeroChat MCP Example\\\", \\\"version\\\": \\\"1.0.0\\\"},\\n \\\"capabilities\\\": {\\\"tools\\\": {}}\\n })\\n elif method == \\\"tools/list\\\":\\n reply(req_id, {\\\"tools\\\": [{\\n \\\"name\\\": \\\"echo\\\",\\n \\\"description\\\": \\\"Echo back a message for testing.\\\",\\n \\\"inputSchema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\\"message\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Message to echo.\\\"}},\\n \\\"required\\\": [\\\"message\\\"]\\n }\\n }]})\\n elif method == \\\"tools/call\\\":\\n if params.get(\\\"name\\\") != \\\"echo\\\":\\n reply(req_id, error={\\\"code\\\": -32601, \\\"message\\\": \\\"Tool not found\\\"})\\n continue\\n msg = params.get(\\\"arguments\\\", {}).get(\\\"message\\\", \\\"\\\")\\n reply(req_id, {\\n \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": f\\\"echo: {msg}\\\"}],\\n \\\"isError\\\": False\\n })\\n\",\n \"ejemplo/installer.json.example\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@scope/mcp-package\\\",\\n \\\"version\\\": \\\"1.2.3\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"ejemplo/README.md\": \"# Crear un MCP gestionado por ZeroChat\\n\\nEsta carpeta es la referencia para crear servicios MCP gestionados. La ubicación de producción es el directorio personal del usuario, seguido de `zerochat/services` (por ejemplo, `/home/usuario/zerochat/services`). No uses `~`, `$HOME` ni una ruta relativa como `services/`: primero identifica la ruta absoluta del directorio personal y úsala para localizar este archivo. El nuevo servicio se crea como carpeta hermana de `ejemplo`, nunca dentro de ella.\\n\\n## Procedimiento obligatorio\\n\\n1. Lee `README.md`, `service.json` y `installer.json.example` de la carpeta `ejemplo` situada en el directorio de datos de ZeroChat.\\n2. Consulta solo la documentación oficial del MCP solicitado.\\n3. Crea una carpeta con un identificador seguro y descriptivo junto a `ejemplo`. Usa un `id` único en `service.json`, también cuando copies otro servicio.\\n4. Crea `service.json` copiando esta estructura y adapta únicamente los valores necesarios.\\n5. Crea `installer.json` solo si el MCP se distribuye como paquete npm. Copia `installer.json.example`, fija la versión exacta y declara la versión mínima de Node.js.\\n6. Añade `help.url` con la página oficial de instalación y etiquetas en español e inglés.\\n7. Si requiere una API key, declara la variable que exige el MCP en el objeto `env` de `launch` dentro de `service.json` con el valor literal `PASTE_API_KEY_HERE`. No pidas la clave al usuario ni la escribas en el chat: indica la ruta absoluta del JSON y el campo que debe editar a mano. El usuario guardará su clave únicamente en el JSON local de su carpeta propia.\\n8. No instales ni inicies el servicio. Al terminar, informa de los archivos creados, fuentes oficiales y requisitos pendientes.\\n\\n## Límites de ZeroChat\\n\\nZeroChat gestiona procesos MCP por `stdio`. Puede instalar paquetes npm mediante `installer.json` y arrancarlos con `${nodeExecutable}`. También puede arrancar un ejecutable o script ya disponible con `launch`.\\n\\nZeroChat también puede usar `mcp-remote` como puente para un MCP remoto HTTP/SSE con OAuth o API key. Decláralo como un servicio `stdio`: el proceso local debe ser `mcp-remote`, mientras que `remote` describe el endpoint remoto para la interfaz. Conserva las credenciales y el almacén OAuth dentro de `${serviceDir}`.\\n\\n## Contratos JSON\\n\\n- `service.json` es obligatorio. Conserva `schemaVersion`, `id`, textos bilingües, `transport: \\\"stdio\\\"` y `launch`.\\n- En `launch`, usa `${serviceDir}`, `${nodeExecutable}` o `${pythonExecutable}`; no uses rutas absolutas de usuario.\\n- `launch.handshakeTimeoutSeconds` (15 s por defecto) limita el arranque y `launch.callTimeoutSeconds` (30 s por defecto) cada llamada a una herramienta; súbelo para MCP lentos, como la automatización de navegador.\\n- `installer.json` es opcional y hoy solo admite `type: \\\"npm\\\"`.\\n- `help` es opcional, pero debe incluirse en todo servicio creado:\\n\\n```json\\n\\\"help\\\": {\\n \\\"url\\\": \\\"https://documentacion-oficial.example/install\\\",\\n \\\"label\\\": { \\\"es\\\": \\\"Instalación oficial\\\", \\\"en\\\": \\\"Official installation\\\" }\\n}\\n```\\n\\n## Prompt para el usuario\\n\\nSustituye únicamente `<nombre-del-mcp>`:\\n\\n```text\\nPara instalar el MCP <nombre-del-mcp>, lee primero services/ejemplo/README.md y sigue exactamente sus instrucciones.\\n```\\n\\n## Servicios remotos y claves API\\n\\n`launch.env` es el objeto `env` dentro de `launch` en `service.json`, no un archivo. El agente deja únicamente el marcador; el usuario introduce la clave real a mano en ese JSON. No crees un archivo `launch.env` o `.env`.\\n\\nPara un remoto, lee también `service.json` e `installer.json` de la carpeta hermana `composio`. Crea una carpeta nueva con nombre e `id` propios; usa Composio como ejemplo del puente local, conservando `transport: \\\"stdio\\\"`. Consulta la documentación oficial del proveedor y de [mcp-remote](https://github.com/punkpeye/mcp-remote#custom-headers) para adaptar endpoint, transporte, cabeceras y autenticación. No copies sin verificar las opciones `--protocol`, los metadatos de cliente ni los parámetros OAuth específicos de Composio. Conserva `HOME` y `USERPROFILE` dentro de `${serviceDir}`.\\n\\nPara un MCP local que requiera una variable, deja este campo dentro de `launch`, conservando sus demás campos y usando el nombre de variable documentado por el MCP:\\n\\n```json\\n\\\"env\\\": {\\n \\\"SERVICE_API_KEY\\\": \\\"PASTE_API_KEY_HERE\\\"\\n}\\n```\\n\\nPara un remoto que acepte `Authorization: Bearer`, los campos dentro de `launch` pueden ser:\\n\\n```json\\n\\\"args\\\": [\\n \\\"${serviceDir}/node_modules/mcp-remote/dist/proxy.js\\\",\\n \\\"https://mcp.example.com/mcp\\\",\\n \\\"--header\\\",\\n \\\"Authorization: Bearer ${REMOTE_API_KEY}\\\"\\n],\\n\\\"env\\\": {\\n \\\"HOME\\\": \\\"${serviceDir}\\\",\\n \\\"USERPROFILE\\\": \\\"${serviceDir}\\\",\\n \\\"REMOTE_API_KEY\\\": \\\"PASTE_API_KEY_HERE\\\"\\n}\\n```\\n\\nAdapta la cabecera y su formato a lo que exige el proveedor. `remote.authentication` solo describe el servicio; no configura la autenticación. La referencia `${REMOTE_API_KEY}` en la cabecera la resuelve `mcp-remote`. ZeroChat no resuelve variables de entorno genéricas: en `launch.env`, `${SERVICE_API_KEY}` se pasaría como texto literal. Solo sustituye `${serviceDir}`, `${nodeExecutable}`, `${pythonExecutable}` y `${option:...}`. No pongas claves reales en argumentos ni en URLs.\\n\\n### Pasos para el usuario\\n\\n1. Usa una carpeta propia bajo el directorio de servicios, con nombre e `id` únicos. Si copias un servicio incluido, copia sus definiciones, no sus dependencias ni credenciales OAuth. No edites `ejemplo`, `composio`, `playwright` ni `memory`: sus archivos distribuidos se restauran al arrancar.\\n2. Abre el `service.json` de tu carpeta y sustituye `PASTE_API_KEY_HERE` por la clave real en `launch.env`. Mantén el JSON válido y los demás campos. No necesitas `export`, `.env` ni otro JSON de credenciales.\\n3. Restringe el archivo a tu usuario con `chmod 600 /ruta/absoluta/zerochat/services/mi-servicio/service.json` en Linux o Termux. La clave queda en texto plano: no compartas el archivo ni lo subas al repositorio o al chat.\\n4. Inicia el servicio desde Ajustes → MCP. Si la nueva carpeta no aparece, reinicia ZeroChat y conecta de nuevo. Para cambiar una clave, detén primero el servicio, edita el JSON y vuelve a iniciarlo.\\n5. ZeroChat conserva los servicios propios al reiniciar y actualizar. Borrar el directorio de datos elimina también esas claves: guarda una copia privada si la necesitas.\\n\\n## English: creating local and remote services\\n\\n1. Locate the user's absolute home directory and read `README.md`, `service.json` and `installer.json.example` in `zerochat/services/ejemplo`.\\n2. Read the requested MCP's official documentation. Create a sibling folder with its own name and unique JSON `id`; do not modify bundled definitions. Preserve bilingual text, `schemaVersion`, `transport: \\\"stdio\\\"`, `launch` and official `help` links. `launch.handshakeTimeoutSeconds` (default 15 s) bounds startup and `launch.callTimeoutSeconds` (default 30 s) each tool call; raise it for slow MCPs such as browser automation.\\n3. For npm services, add `installer.json` with an exact package version and minimum Node.js major version. Otherwise use an existing executable or script.\\n4. For remote HTTP/SSE MCPs, also read the sibling `composio` JSON files and use its managed `mcp-remote` bridge as the example. Adapt endpoint, transport and authentication to the official provider and bridge documentation. Verify Composio-specific protocol and OAuth options before copying them. Keep `HOME` and `USERPROFILE` isolated inside `${serviceDir}`.\\n5. If an API key is required, put the literal `PASTE_API_KEY_HERE` under the required variable in the `env` object under `launch` inside `service.json`. `launch.env` is a JSON field, not a file. The user enters the actual key manually in that JSON; do not create a `launch.env` or `.env` file. Do not request the user's key or place real secrets in chat, arguments or URLs. Give the absolute JSON path and exact field to edit. For remote header authentication, use `--header` with the provider's required header and a reference to the variable, as shown above; `mcp-remote` resolves that reference. `remote.authentication` is descriptive only.\\n6. Do not install or start the service. Report created files, official sources and outstanding requirements; the user will start it from Settings → MCP.\\n\\nThe user replaces `PASTE_API_KEY_HERE` directly in their own folder's JSON, preserves valid JSON and existing fields, and restricts the file with `chmod 600 /absolute/path/zerochat/services/my-service/service.json` on Linux/Termux. No shell export, `.env` or separate credentials JSON is needed. ZeroChat only expands service/interpreter paths and `${option:...}`, not generic environment placeholders in `launch.env`. The key is stored in plain text: do not share or commit the file or paste it into chat.\\n\\nIf a new folder does not appear, restart ZeroChat and reconnect. Stop an active service before editing its key, then start it again. User-created folders survive restarts and updates. Bundled definitions are restored on startup: copy them into a new folder and change the JSON `id` to customize them, without copying dependencies or OAuth credentials. Deleting the data directory also deletes custom services and their keys; keep a private backup if needed.\\n\",\n \"ejemplo/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"ejemplo\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Ejemplo de MCP\\\",\\n \\\"en\\\": \\\"MCP example\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Plantilla mínima para crear servicios MCP gestionados por ZeroChat. Muestra el protocolo stdio y la estructura de configuración que debe seguir un nuevo MCP.\\\",\\n \\\"en\\\": \\\"Minimal template for creating ZeroChat-managed MCP services. It demonstrates the stdio protocol and configuration structure for a new MCP.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"help/mcp.html#crear-mcp-con-agente\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía para crear un MCP gestionado\\\",\\n \\\"en\\\": \\\"Guide to creating a managed MCP\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${pythonExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/ejemplo_mcp_server.py\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 10\\n }\\n}\\n\",\n \"memory/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@modelcontextprotocol/server-memory\\\",\\n \\\"version\\\": \\\"2026.8.31\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"memory/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"memory\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Memoria y Grafos (Knowledge Graph)\\\",\\n \\\"en\\\": \\\"Memory & Knowledge Graph\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Almacenamiento persistente de entidades, preferencias y contexto histórico estructurado en un grafo de conocimiento.\\\",\\n \\\"en\\\": \\\"Persistent storage of entities, preferences, and historical context structured as a knowledge graph.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@modelcontextprotocol/server-memory/dist/index.js\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"MEMORY_FILE_PATH\\\": \\\"${serviceDir}/memory.jsonl\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 30\\n }\\n}\\n\\n\",\n \"playwright/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@playwright/mcp\\\",\\n \\\"version\\\": \\\"0.0.81\\\",\\n \\\"browser\\\": \\\"chromium\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"playwright/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"playwright\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Playwright MCP\\\",\\n \\\"en\\\": \\\"Playwright MCP\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Automatización de navegador mediante el servidor MCP oficial de Playwright.\\\",\\n \\\"en\\\": \\\"Browser automation through the official Playwright MCP server.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@playwright/mcp/cli.js\\\", \\\"--browser=chromium\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 30,\\n \\\"callTimeoutSeconds\\\": 120\\n },\\n \\\"options\\\": [\\n {\\n \\\"id\\\": \\\"headless\\\",\\n \\\"type\\\": \\\"boolean\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Navegación en segundo plano (Headless)\\\",\\n \\\"en\\\": \\\"Headless background mode\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Desactívalo para ver la ventana del navegador durante la automatización\\\",\\n \\\"en\\\": \\\"Disable to display the browser window during automation\\\"\\n },\\n \\\"default\\\": true,\\n \\\"argsWhenTrue\\\": [\\\"--headless\\\"],\\n \\\"argsWhenFalse\\\": []\\n }\\n ]\\n}\\n\\n\"\n}")
|
|
1104
|
+
MANAGED_SERVICE_FILES: dict[str, str] = json.loads("{\n \"composio/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"mcp-remote\\\",\\n \\\"version\\\": \\\"0.13.5\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"composio/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"composio\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"MCP remoto para acceder a aplicaciones como Gmail, Google Drive, Slack o GitHub. Composio solicita la autorización de cada aplicación cuando sea necesaria.\\\",\\n \\\"en\\\": \\\"Remote MCP for apps such as Gmail, Google Drive, Slack, and GitHub. Composio requests authorization for each app when it is needed.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"https://docs.composio.dev/docs/composio-connect\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía oficial de Composio Connect\\\",\\n \\\"en\\\": \\\"Composio Connect official guide\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"remote\\\": {\\n \\\"type\\\": \\\"mcp-remote\\\",\\n \\\"transport\\\": \\\"streamable-http\\\",\\n \\\"url\\\": \\\"https://connect.composio.dev/mcp\\\",\\n \\\"authentication\\\": \\\"composio-connect\\\"\\n },\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\n \\\"${serviceDir}/node_modules/mcp-remote/dist/proxy.js\\\",\\n \\\"https://connect.composio.dev/mcp\\\",\\n \\\"--protocol\\\",\\n \\\"legacy\\\",\\n \\\"--static-oauth-client-metadata\\\",\\n \\\"{\\\\\\\"client_name\\\\\\\":\\\\\\\"zerochat-mcp-remote\\\\\\\"}\\\",\\n \\\"--auth-timeout\\\",\\n \\\"120\\\"\\n ],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"HOME\\\": \\\"${serviceDir}\\\",\\n \\\"USERPROFILE\\\": \\\"${serviceDir}\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 150\\n }\\n}\\n\",\n \"ejemplo/ejemplo_mcp_server.py\": \"#!/usr/bin/env python3\\nimport json\\nimport sys\\n\\ndef reply(req_id, result=None, error=None):\\n resp = {\\\"jsonrpc\\\": \\\"2.0\\\", \\\"id\\\": req_id}\\n if error:\\n resp[\\\"error\\\"] = error\\n else:\\n resp[\\\"result\\\"] = result\\n sys.stdout.write(json.dumps(resp) + \\\"\\\\n\\\")\\n sys.stdout.flush()\\n\\nfor raw in sys.stdin:\\n try:\\n req = json.loads(raw)\\n except Exception:\\n continue\\n req_id = req.get(\\\"id\\\")\\n method = req.get(\\\"method\\\")\\n params = req.get(\\\"params\\\", {})\\n if method == \\\"initialize\\\":\\n reply(req_id, {\\n \\\"protocolVersion\\\": \\\"2024-11-05\\\",\\n \\\"serverInfo\\\": {\\\"name\\\": \\\"ZeroChat MCP Example\\\", \\\"version\\\": \\\"1.0.0\\\"},\\n \\\"capabilities\\\": {\\\"tools\\\": {}}\\n })\\n elif method == \\\"tools/list\\\":\\n reply(req_id, {\\\"tools\\\": [{\\n \\\"name\\\": \\\"echo\\\",\\n \\\"description\\\": \\\"Echo back a message for testing.\\\",\\n \\\"inputSchema\\\": {\\n \\\"type\\\": \\\"object\\\",\\n \\\"properties\\\": {\\\"message\\\": {\\\"type\\\": \\\"string\\\", \\\"description\\\": \\\"Message to echo.\\\"}},\\n \\\"required\\\": [\\\"message\\\"]\\n }\\n }]})\\n elif method == \\\"tools/call\\\":\\n if params.get(\\\"name\\\") != \\\"echo\\\":\\n reply(req_id, error={\\\"code\\\": -32601, \\\"message\\\": \\\"Tool not found\\\"})\\n continue\\n msg = params.get(\\\"arguments\\\", {}).get(\\\"message\\\", \\\"\\\")\\n reply(req_id, {\\n \\\"content\\\": [{\\\"type\\\": \\\"text\\\", \\\"text\\\": f\\\"echo: {msg}\\\"}],\\n \\\"isError\\\": False\\n })\\n\",\n \"ejemplo/installer.json.example\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@scope/mcp-package\\\",\\n \\\"version\\\": \\\"1.2.3\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"ejemplo/README.md\": \"# Crear un MCP gestionado por ZeroChat\\n\\nEsta carpeta es la referencia para crear servicios MCP gestionados. La ubicación de producción es el directorio personal del usuario, seguido de `zerochat/services` (por ejemplo, `/home/usuario/zerochat/services`). No uses `~`, `$HOME` ni una ruta relativa como `services/`: primero identifica la ruta absoluta del directorio personal y úsala para localizar este archivo. El nuevo servicio se crea como carpeta hermana de `ejemplo`, nunca dentro de ella.\\n\\n## Procedimiento obligatorio\\n\\n1. Lee `README.md`, `service.json` y `installer.json.example` de la carpeta `ejemplo` situada en el directorio de datos de ZeroChat.\\n2. Consulta solo la documentación oficial del MCP solicitado.\\n3. Crea una carpeta con un identificador seguro y descriptivo junto a `ejemplo`. Usa un `id` único en `service.json`, también cuando copies otro servicio.\\n4. Crea `service.json` copiando esta estructura y adapta únicamente los valores necesarios.\\n5. Crea `installer.json` solo si el MCP se distribuye como paquete npm. Copia `installer.json.example`, fija la versión exacta y declara la versión mínima de Node.js.\\n6. Añade `help.url` con la página oficial de instalación y etiquetas en español e inglés.\\n7. Si requiere una API key, declara la variable que exige el MCP en el objeto `env` de `launch` dentro de `service.json` con el valor literal `PASTE_API_KEY_HERE`. No pidas la clave al usuario ni la escribas en el chat: indica la ruta absoluta del JSON y el campo que debe editar a mano. El usuario guardará su clave únicamente en el JSON local de su carpeta propia.\\n8. No instales ni inicies el servicio. Al terminar, informa de los archivos creados, fuentes oficiales y requisitos pendientes.\\n\\n## Límites de ZeroChat\\n\\nZeroChat gestiona procesos MCP por `stdio`. Puede instalar paquetes npm mediante `installer.json` y arrancarlos con `${nodeExecutable}`. También puede arrancar un ejecutable o script ya disponible con `launch`.\\n\\nZeroChat también puede usar `mcp-remote` como puente para un MCP remoto HTTP/SSE con OAuth o API key. Decláralo como un servicio `stdio`: el proceso local debe ser `mcp-remote`, mientras que `remote` describe el endpoint remoto para la interfaz. Conserva las credenciales y el almacén OAuth dentro de `${serviceDir}`.\\n\\n## Contratos JSON\\n\\n- `service.json` es obligatorio. Conserva `schemaVersion`, `id`, textos bilingües, `transport: \\\"stdio\\\"` y `launch`.\\n- En `launch`, usa `${serviceDir}`, `${nodeExecutable}` o `${pythonExecutable}`; no uses rutas absolutas de usuario.\\n- `launch.handshakeTimeoutSeconds` (15 s por defecto) limita el arranque y `launch.callTimeoutSeconds` (30 s por defecto) cada llamada a una herramienta; súbelo para MCP lentos, como la automatización de navegador.\\n- `installer.json` es opcional y hoy solo admite `type: \\\"npm\\\"`.\\n- `help` es opcional, pero debe incluirse en todo servicio creado:\\n\\n```json\\n\\\"help\\\": {\\n \\\"url\\\": \\\"https://documentacion-oficial.example/install\\\",\\n \\\"label\\\": { \\\"es\\\": \\\"Instalación oficial\\\", \\\"en\\\": \\\"Official installation\\\" }\\n}\\n```\\n\\n## Prompt para el usuario\\n\\nSustituye únicamente `<nombre-del-mcp>`:\\n\\n```text\\nPara instalar el MCP <nombre-del-mcp>, lee primero services/ejemplo/README.md y sigue exactamente sus instrucciones.\\n```\\n\\n## Servicios remotos y claves API\\n\\n`launch.env` es el objeto `env` dentro de `launch` en `service.json`, no un archivo. El agente deja únicamente el marcador; el usuario introduce la clave real a mano en ese JSON. No crees un archivo `launch.env` o `.env`.\\n\\nPara un remoto, lee también `service.json` e `installer.json` de la carpeta hermana `composio`. Crea una carpeta nueva con nombre e `id` propios; usa Composio como ejemplo del puente local, conservando `transport: \\\"stdio\\\"`. Consulta la documentación oficial del proveedor y de [mcp-remote](https://github.com/punkpeye/mcp-remote#custom-headers) para adaptar endpoint, transporte, cabeceras y autenticación. No copies sin verificar las opciones `--protocol`, los metadatos de cliente ni los parámetros OAuth específicos de Composio. Conserva `HOME` y `USERPROFILE` dentro de `${serviceDir}`.\\n\\nPara un MCP local que requiera una variable, deja este campo dentro de `launch`, conservando sus demás campos y usando el nombre de variable documentado por el MCP:\\n\\n```json\\n\\\"env\\\": {\\n \\\"SERVICE_API_KEY\\\": \\\"PASTE_API_KEY_HERE\\\"\\n}\\n```\\n\\nPara un remoto que acepte `Authorization: Bearer`, los campos dentro de `launch` pueden ser:\\n\\n```json\\n\\\"args\\\": [\\n \\\"${serviceDir}/node_modules/mcp-remote/dist/proxy.js\\\",\\n \\\"https://mcp.example.com/mcp\\\",\\n \\\"--header\\\",\\n \\\"Authorization: Bearer ${REMOTE_API_KEY}\\\"\\n],\\n\\\"env\\\": {\\n \\\"HOME\\\": \\\"${serviceDir}\\\",\\n \\\"USERPROFILE\\\": \\\"${serviceDir}\\\",\\n \\\"REMOTE_API_KEY\\\": \\\"PASTE_API_KEY_HERE\\\"\\n}\\n```\\n\\nAdapta la cabecera y su formato a lo que exige el proveedor. `remote.authentication` solo describe el servicio; no configura la autenticación. La referencia `${REMOTE_API_KEY}` en la cabecera la resuelve `mcp-remote`. ZeroChat no resuelve variables de entorno genéricas: en `launch.env`, `${SERVICE_API_KEY}` se pasaría como texto literal. Solo sustituye `${serviceDir}`, `${nodeExecutable}`, `${pythonExecutable}` y `${option:...}`. No pongas claves reales en argumentos ni en URLs.\\n\\n### Pasos para el usuario\\n\\n1. Usa una carpeta propia bajo el directorio de servicios, con nombre e `id` únicos. Si copias un servicio incluido, copia sus definiciones, no sus dependencias ni credenciales OAuth. No edites `ejemplo`, `composio`, `playwright` ni `memory`: sus archivos distribuidos se restauran al arrancar.\\n2. Abre el `service.json` de tu carpeta y sustituye `PASTE_API_KEY_HERE` por la clave real en `launch.env`. Mantén el JSON válido y los demás campos. No necesitas `export`, `.env` ni otro JSON de credenciales.\\n3. Restringe el archivo a tu usuario con `chmod 600 /ruta/absoluta/zerochat/services/mi-servicio/service.json` en Linux o Termux. La clave queda en texto plano: no compartas el archivo ni lo subas al repositorio o al chat.\\n4. Inicia el servicio desde Ajustes → MCP. Si la nueva carpeta no aparece, reinicia ZeroChat y conecta de nuevo. Para cambiar una clave, detén primero el servicio, edita el JSON y vuelve a iniciarlo.\\n5. ZeroChat conserva los servicios propios al reiniciar y actualizar. Borrar el directorio de datos elimina también esas claves: guarda una copia privada si la necesitas.\\n\\n## English: creating local and remote services\\n\\n1. Locate the user's absolute home directory and read `README.md`, `service.json` and `installer.json.example` in `zerochat/services/ejemplo`.\\n2. Read the requested MCP's official documentation. Create a sibling folder with its own name and unique JSON `id`; do not modify bundled definitions. Preserve bilingual text, `schemaVersion`, `transport: \\\"stdio\\\"`, `launch` and official `help` links. `launch.handshakeTimeoutSeconds` (default 15 s) bounds startup and `launch.callTimeoutSeconds` (default 30 s) each tool call; raise it for slow MCPs such as browser automation.\\n3. For npm services, add `installer.json` with an exact package version and minimum Node.js major version. Otherwise use an existing executable or script.\\n4. For remote HTTP/SSE MCPs, also read the sibling `composio` JSON files and use its managed `mcp-remote` bridge as the example. Adapt endpoint, transport and authentication to the official provider and bridge documentation. Verify Composio-specific protocol and OAuth options before copying them. Keep `HOME` and `USERPROFILE` isolated inside `${serviceDir}`.\\n5. If an API key is required, put the literal `PASTE_API_KEY_HERE` under the required variable in the `env` object under `launch` inside `service.json`. `launch.env` is a JSON field, not a file. The user enters the actual key manually in that JSON; do not create a `launch.env` or `.env` file. Do not request the user's key or place real secrets in chat, arguments or URLs. Give the absolute JSON path and exact field to edit. For remote header authentication, use `--header` with the provider's required header and a reference to the variable, as shown above; `mcp-remote` resolves that reference. `remote.authentication` is descriptive only.\\n6. Do not install or start the service. Report created files, official sources and outstanding requirements; the user will start it from Settings → MCP.\\n\\nThe user replaces `PASTE_API_KEY_HERE` directly in their own folder's JSON, preserves valid JSON and existing fields, and restricts the file with `chmod 600 /absolute/path/zerochat/services/my-service/service.json` on Linux/Termux. No shell export, `.env` or separate credentials JSON is needed. ZeroChat only expands service/interpreter paths and `${option:...}`, not generic environment placeholders in `launch.env`. The key is stored in plain text: do not share or commit the file or paste it into chat.\\n\\nIf a new folder does not appear, restart ZeroChat and reconnect. Stop an active service before editing its key, then start it again. User-created folders survive restarts and updates. Bundled definitions are restored on startup: copy them into a new folder and change the JSON `id` to customize them, without copying dependencies or OAuth credentials. Deleting the data directory also deletes custom services and their keys; keep a private backup if needed.\\n\",\n \"ejemplo/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"ejemplo\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Ejemplo de MCP\\\",\\n \\\"en\\\": \\\"MCP example\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Plantilla mínima para crear servicios MCP gestionados por ZeroChat. Muestra el protocolo stdio y la estructura de configuración que debe seguir un nuevo MCP.\\\",\\n \\\"en\\\": \\\"Minimal template for creating ZeroChat-managed MCP services. It demonstrates the stdio protocol and configuration structure for a new MCP.\\\"\\n },\\n \\\"help\\\": {\\n \\\"url\\\": \\\"help/mcp.html#crear-mcp-con-agente\\\",\\n \\\"label\\\": {\\n \\\"es\\\": \\\"Guía para crear un MCP gestionado\\\",\\n \\\"en\\\": \\\"Guide to creating a managed MCP\\\"\\n }\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${pythonExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/ejemplo_mcp_server.py\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 10\\n }\\n}\\n\",\n \"memory/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@modelcontextprotocol/server-memory\\\",\\n \\\"version\\\": \\\"2026.8.31\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"memory/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"memory\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Memoria y Grafos (Knowledge Graph)\\\",\\n \\\"en\\\": \\\"Memory & Knowledge Graph\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Almacenamiento persistente de entidades, preferencias y contexto histórico estructurado en un grafo de conocimiento.\\\",\\n \\\"en\\\": \\\"Persistent storage of entities, preferences, and historical context structured as a knowledge graph.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@modelcontextprotocol/server-memory/dist/index.js\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {\\n \\\"MEMORY_FILE_PATH\\\": \\\"${serviceDir}/memory.jsonl\\\"\\n },\\n \\\"handshakeTimeoutSeconds\\\": 30\\n }\\n}\\n\\n\",\n \"playwright/installer.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"type\\\": \\\"npm\\\",\\n \\\"product\\\": {\\n \\\"package\\\": \\\"@playwright/mcp\\\",\\n \\\"version\\\": \\\"0.0.81\\\",\\n \\\"browser\\\": \\\"chromium\\\"\\n },\\n \\\"requirements\\\": {\\n \\\"node\\\": {\\n \\\"minimumMajor\\\": 18\\n }\\n }\\n}\\n\",\n \"playwright/service.json\": \"{\\n \\\"schemaVersion\\\": 1,\\n \\\"id\\\": \\\"playwright\\\",\\n \\\"displayName\\\": {\\n \\\"es\\\": \\\"Playwright MCP\\\",\\n \\\"en\\\": \\\"Playwright MCP\\\"\\n },\\n \\\"description\\\": {\\n \\\"es\\\": \\\"Automatización de navegador mediante el servidor MCP oficial de Playwright.\\\",\\n \\\"en\\\": \\\"Browser automation through the official Playwright MCP server.\\\"\\n },\\n \\\"enabledByDefault\\\": false,\\n \\\"transport\\\": \\\"stdio\\\",\\n \\\"launch\\\": {\\n \\\"executable\\\": \\\"${nodeExecutable}\\\",\\n \\\"args\\\": [\\\"${serviceDir}/node_modules/@playwright/mcp/cli.js\\\"],\\n \\\"cwd\\\": \\\"${serviceDir}\\\",\\n \\\"env\\\": {},\\n \\\"handshakeTimeoutSeconds\\\": 30,\\n \\\"callTimeoutSeconds\\\": 120\\n }\\n}\\n\"\n}")
|
|
1105
1105
|
|
|
1106
1106
|
|
|
1107
1107
|
def materialize_managed_services(services_root: Path | None = None) -> None:
|
|
@@ -1727,19 +1727,13 @@ class PersistentBrowserSession:
|
|
|
1727
1727
|
const readline = require('readline');
|
|
1728
1728
|
let playwright;
|
|
1729
1729
|
try {
|
|
1730
|
-
playwright = require(
|
|
1731
|
-
} catch (
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
console.log(JSON.stringify({
|
|
1738
|
-
success: false,
|
|
1739
|
-
error: "Playwright is not available. Install it with 'npm install playwright' or enable the Playwright MCP service."
|
|
1740
|
-
}));
|
|
1741
|
-
process.exit(1);
|
|
1742
|
-
}
|
|
1730
|
+
playwright = require(process.env.ZEROCHAT_PLAYWRIGHT_MODULE);
|
|
1731
|
+
} catch (e) {
|
|
1732
|
+
console.log(JSON.stringify({
|
|
1733
|
+
success: false,
|
|
1734
|
+
error: "Playwright is not available. Start the Playwright service once in Settings > MCP to install it."
|
|
1735
|
+
}));
|
|
1736
|
+
process.exit(1);
|
|
1743
1737
|
}
|
|
1744
1738
|
|
|
1745
1739
|
(async () => {
|
|
@@ -1748,8 +1742,8 @@ try {
|
|
|
1748
1742
|
const candidates = [
|
|
1749
1743
|
{}, // 1. Playwright bundled Chromium
|
|
1750
1744
|
{ channel: 'chrome' }, // 2. System Google Chrome
|
|
1751
|
-
{ channel: 'msedge' }, // 3. System Microsoft Edge
|
|
1752
|
-
{ channel: 'chromium' } // 4.
|
|
1745
|
+
{ channel: 'msedge' }, // 3. System Microsoft Edge
|
|
1746
|
+
{ channel: 'chromium' } // 4. Playwright full Chromium, if only the headless shell is missing
|
|
1753
1747
|
];
|
|
1754
1748
|
let lastErr = null;
|
|
1755
1749
|
for (const cand of candidates) {
|
|
@@ -1841,6 +1835,7 @@ try {
|
|
|
1841
1835
|
"""
|
|
1842
1836
|
proc = subprocess.Popen(
|
|
1843
1837
|
[node, "-e", runner_js],
|
|
1838
|
+
env={**os.environ, "ZEROCHAT_PLAYWRIGHT_MODULE": str(playwright_service_module())},
|
|
1844
1839
|
stdin=subprocess.PIPE,
|
|
1845
1840
|
stdout=subprocess.PIPE,
|
|
1846
1841
|
stderr=subprocess.PIPE,
|
|
@@ -1904,34 +1899,21 @@ def browser_action_availability() -> dict:
|
|
|
1904
1899
|
"""Comprueba requisitos locales sin lanzar Node.js ni Chromium."""
|
|
1905
1900
|
if not shutil.which("node"):
|
|
1906
1901
|
return {"available": False, "error": "Node.js is not installed or not found in system PATH."}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
get_data_dir() / "services" / "playwright" / "node_modules" / "playwright"
|
|
1910
|
-
)
|
|
1911
|
-
if not any(location.is_dir() for location in playwright_locations):
|
|
1902
|
+
# Solo el Playwright del servicio, no el node_modules de la carpeta desde la que arranca el servidor.
|
|
1903
|
+
if not playwright_service_cli().is_file():
|
|
1912
1904
|
return {"available": False, "error": "Playwright is not available."}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
)
|
|
1917
|
-
if sys.platform == "darwin":
|
|
1918
|
-
playwright_cache = Path.home() / "Library" / "Caches" / "ms-playwright"
|
|
1919
|
-
elif sys.platform.startswith("win"):
|
|
1920
|
-
playwright_cache = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "ms-playwright"
|
|
1921
|
-
else:
|
|
1922
|
-
playwright_cache = Path.home() / ".cache" / "ms-playwright"
|
|
1923
|
-
try:
|
|
1924
|
-
has_playwright_browser = playwright_cache.is_dir() and any(
|
|
1925
|
-
child.is_dir() and child.name.startswith("chromium")
|
|
1926
|
-
for child in playwright_cache.iterdir()
|
|
1927
|
-
)
|
|
1928
|
-
except OSError:
|
|
1929
|
-
has_playwright_browser = False
|
|
1930
|
-
if has_playwright_browser or any(shutil.which(command) for command in browser_commands):
|
|
1905
|
+
# El runner prueba las entradas de Chromium de Playwright y los canales chrome y msedge.
|
|
1906
|
+
browsers = system_browsers()
|
|
1907
|
+
if cached_engine_entries() & set(ENGINE_CACHE_ENTRIES["chromium"]) or browsers["chrome"] or browsers["msedge"]:
|
|
1931
1908
|
return {"available": True}
|
|
1932
1909
|
return {"available": False, "error": "Playwright Chromium is not installed."}
|
|
1933
1910
|
|
|
1934
1911
|
|
|
1912
|
+
def playwright_status() -> dict:
|
|
1913
|
+
"""Playwright del servicio y browser_action tal como están ahora, para el latido del cliente."""
|
|
1914
|
+
return {"installed": playwright_service_cli().is_file(), "browserAction": browser_action_availability()}
|
|
1915
|
+
|
|
1916
|
+
|
|
1935
1917
|
def browser_action(action: str, url: str | None = None, selector: str | None = None, value: str | None = None) -> str:
|
|
1936
1918
|
"""Control a headless browser for UI testing and visual inspection."""
|
|
1937
1919
|
try:
|
|
@@ -2113,6 +2095,352 @@ LOCAL_TOOL_HANDLERS = {
|
|
|
2113
2095
|
"get_diagnostics": get_diagnostics,
|
|
2114
2096
|
"browser_action": browser_action
|
|
2115
2097
|
}
|
|
2098
|
+
# Configuración del navegador de Playwright MCP (sección Navegador de Ajustes).
|
|
2099
|
+
# Modo propio: un motor de Playwright con perfil persistente en ~/zerochat/playwright/profile/<motor>.
|
|
2100
|
+
# Modo del usuario: su Chrome, Edge o Chromium mediante la extensión de Playwright (--extension).
|
|
2101
|
+
BROWSER_MODES = ("own", "user")
|
|
2102
|
+
OWN_BROWSER_ENGINES = ("chromium", "firefox", "webkit")
|
|
2103
|
+
USER_BROWSERS = ("chrome", "msedge", "chromium")
|
|
2104
|
+
BROWSER_CONFIG_DEFAULTS = {"mode": "own", "engine": "chromium", "userBrowser": "chrome", "headless": True, "extensionToken": ""}
|
|
2105
|
+
# Identificador de la extensión de Playwright en la tienda de Chrome, que también usa Edge.
|
|
2106
|
+
PLAYWRIGHT_EXTENSION_ID = "mmlmfjhmonkocbjadbfplnigmagldckm"
|
|
2107
|
+
# Token que muestra la extensión: con él la conexión no pide aceptarla en el navegador.
|
|
2108
|
+
_EXTENSION_TOKEN = re.compile(r"^[A-Za-z0-9_-]{0,256}$")
|
|
2109
|
+
_BROWSER_CONFIG_CHOICES = {"mode": BROWSER_MODES, "engine": OWN_BROWSER_ENGINES, "userBrowser": USER_BROWSERS}
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def browser_config_file() -> Path:
|
|
2113
|
+
return get_data_dir() / "config" / "browser.json"
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def own_browser_profile_dir(engine: str) -> Path:
|
|
2117
|
+
return get_data_dir() / "playwright" / "profile" / engine
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
def validate_browser_config(raw) -> dict:
|
|
2121
|
+
"""Devuelve la configuración completa o lanza ValueError; los campos que faltan toman su valor por defecto."""
|
|
2122
|
+
if not isinstance(raw, dict):
|
|
2123
|
+
raise ValueError("La configuración del navegador debe ser un objeto")
|
|
2124
|
+
unknown = sorted(set(raw) - set(BROWSER_CONFIG_DEFAULTS))
|
|
2125
|
+
if unknown:
|
|
2126
|
+
raise ValueError(f"Campos desconocidos en la configuración del navegador: {', '.join(unknown)}")
|
|
2127
|
+
config = {**BROWSER_CONFIG_DEFAULTS, **raw}
|
|
2128
|
+
for key, choices in _BROWSER_CONFIG_CHOICES.items():
|
|
2129
|
+
if config[key] not in choices:
|
|
2130
|
+
raise ValueError(f"{key} debe ser uno de: {', '.join(choices)}")
|
|
2131
|
+
if not isinstance(config["headless"], bool):
|
|
2132
|
+
raise ValueError("headless debe ser un booleano")
|
|
2133
|
+
if not isinstance(config["extensionToken"], str) or not _EXTENSION_TOKEN.match(config["extensionToken"]):
|
|
2134
|
+
raise ValueError("extensionToken debe tener hasta 256 letras, cifras, guiones o guiones bajos")
|
|
2135
|
+
return config
|
|
2136
|
+
|
|
2137
|
+
|
|
2138
|
+
def load_browser_config() -> dict:
|
|
2139
|
+
path = browser_config_file()
|
|
2140
|
+
if not path.exists():
|
|
2141
|
+
return dict(BROWSER_CONFIG_DEFAULTS)
|
|
2142
|
+
try:
|
|
2143
|
+
return validate_browser_config(json.loads(path.read_text(encoding="utf-8")))
|
|
2144
|
+
except (OSError, ValueError) as err:
|
|
2145
|
+
log_event(f"[zerochat] Configuración del navegador no válida en {path}, se usan los valores por defecto: {err}")
|
|
2146
|
+
return dict(BROWSER_CONFIG_DEFAULTS)
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def save_browser_config(raw) -> dict:
|
|
2150
|
+
config = validate_browser_config(raw)
|
|
2151
|
+
path = browser_config_file()
|
|
2152
|
+
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
2153
|
+
# Guarda el token de la extensión: solo lo lee el usuario.
|
|
2154
|
+
_write_private_text(path, json.dumps(config, indent=2))
|
|
2155
|
+
return config
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
def _playwright_active() -> bool:
|
|
2159
|
+
return any(server["id"] == "playwright" and server["status"] in ("running", "starting", "installing")
|
|
2160
|
+
for server in GLOBAL_MCP_MANAGER.list_servers())
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def clear_own_browser_data(engine) -> dict:
|
|
2164
|
+
"""Borra el perfil persistente del motor (cookies, sesiones, almacenamiento de los sitios)."""
|
|
2165
|
+
if engine not in OWN_BROWSER_ENGINES:
|
|
2166
|
+
raise ValueError(f"engine debe ser uno de: {', '.join(OWN_BROWSER_ENGINES)}")
|
|
2167
|
+
# El navegador abierto tiene el perfil bloqueado y volvería a escribirlo.
|
|
2168
|
+
if _playwright_active():
|
|
2169
|
+
raise ValueError("Detén Playwright MCP antes de borrar los datos del navegador.")
|
|
2170
|
+
shutil.rmtree(own_browser_profile_dir(engine), ignore_errors=True)
|
|
2171
|
+
return {"cleared": engine}
|
|
2172
|
+
|
|
2173
|
+
|
|
2174
|
+
def chromium_install() -> tuple[str, Path] | None:
|
|
2175
|
+
"""Chromium del sistema: ejecutable y carpeta de datos del usuario, o None si no está."""
|
|
2176
|
+
if sys.platform == "darwin":
|
|
2177
|
+
executable = "/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
2178
|
+
data_dir = Path.home() / "Library" / "Application Support" / "Chromium"
|
|
2179
|
+
elif sys.platform.startswith("win"):
|
|
2180
|
+
local = Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local")
|
|
2181
|
+
executable = str(local / "Chromium" / "Application" / "chrome.exe")
|
|
2182
|
+
data_dir = local / "Chromium" / "User Data"
|
|
2183
|
+
else:
|
|
2184
|
+
executable = shutil.which("chromium") or shutil.which("chromium-browser") or ""
|
|
2185
|
+
# El Chromium de snap guarda el perfil dentro de su confinamiento.
|
|
2186
|
+
snap = os.path.realpath(executable).startswith("/snap/")
|
|
2187
|
+
data_dir = Path.home() / ("snap/chromium/common/chromium" if snap else ".config/chromium")
|
|
2188
|
+
return (executable, data_dir) if executable and os.path.isfile(executable) else None
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
def playwright_launch_args(config: dict) -> list[str]:
|
|
2192
|
+
"""Argumentos de Playwright MCP para la configuración del navegador."""
|
|
2193
|
+
if config["mode"] == "user":
|
|
2194
|
+
# La extensión maneja el navegador abierto del usuario: siempre con ventana.
|
|
2195
|
+
if config["userBrowser"] != "chromium":
|
|
2196
|
+
return ["--extension", f"--browser={config['userBrowser']}"]
|
|
2197
|
+
# Chromium no es un canal de Playwright: se da su ejecutable y su perfil, con el canal chrome
|
|
2198
|
+
# para que no lo lance sin sandbox si no está abierto.
|
|
2199
|
+
install = chromium_install()
|
|
2200
|
+
if not install:
|
|
2201
|
+
raise RuntimeError("No se encuentra Chromium en el sistema: elige otro navegador en Ajustes › Navegador.")
|
|
2202
|
+
executable, data_dir = install
|
|
2203
|
+
return ["--extension", "--browser=chrome", f"--executable-path={executable}", f"--user-data-dir={data_dir}"]
|
|
2204
|
+
profile_dir = own_browser_profile_dir(config["engine"])
|
|
2205
|
+
for directory in (profile_dir.parent.parent, profile_dir.parent, profile_dir):
|
|
2206
|
+
directory.mkdir(mode=0o700, exist_ok=True)
|
|
2207
|
+
args = [f"--browser={config['engine']}", f"--user-data-dir={profile_dir}"]
|
|
2208
|
+
if config["headless"]:
|
|
2209
|
+
args.append("--headless")
|
|
2210
|
+
return args
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
def playwright_launch_env(config: dict) -> dict[str, str]:
|
|
2214
|
+
"""Entorno de Playwright MCP: el token de la extensión, que no aparece en la línea de órdenes ni en el log."""
|
|
2215
|
+
if config["mode"] == "user" and config["extensionToken"]:
|
|
2216
|
+
return {"PLAYWRIGHT_MCP_EXTENSION_TOKEN": config["extensionToken"]}
|
|
2217
|
+
return {}
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
def validate_profile_browser(raw) -> dict:
|
|
2221
|
+
"""Navegador que exige un perfil: el propio con un motor o el del usuario con uno de sus navegadores."""
|
|
2222
|
+
if isinstance(raw, dict) and raw.get("mode") == "own" and set(raw) == {"mode", "engine"} and raw["engine"] in OWN_BROWSER_ENGINES:
|
|
2223
|
+
return {"mode": "own", "engine": raw["engine"]}
|
|
2224
|
+
if isinstance(raw, dict) and raw.get("mode") == "user" and set(raw) == {"mode", "userBrowser"} and raw["userBrowser"] in USER_BROWSERS:
|
|
2225
|
+
return {"mode": "user", "userBrowser": raw["userBrowser"]}
|
|
2226
|
+
raise ValueError("browser debe ser {mode: own, engine} o {mode: user, userBrowser} con un motor o navegador admitido")
|
|
2227
|
+
|
|
2228
|
+
|
|
2229
|
+
def browser_choice(config: dict) -> dict:
|
|
2230
|
+
"""Modo y navegador con los que arranca Playwright MCP, en la forma de validate_profile_browser."""
|
|
2231
|
+
if config["mode"] == "own":
|
|
2232
|
+
return {"mode": "own", "engine": config["engine"]}
|
|
2233
|
+
return {"mode": "user", "userBrowser": config["userBrowser"]}
|
|
2234
|
+
|
|
2235
|
+
|
|
2236
|
+
def _playwright_launch(browser: dict | None) -> tuple[list[str], dict[str, str], dict]:
|
|
2237
|
+
"""La configuración de Ajustes › Navegador con el modo y el navegador que exija el perfil activo."""
|
|
2238
|
+
config = load_browser_config()
|
|
2239
|
+
if browser is not None:
|
|
2240
|
+
config = {**config, **validate_profile_browser(browser)}
|
|
2241
|
+
return playwright_launch_args(config), playwright_launch_env(config), browser_choice(config)
|
|
2242
|
+
|
|
2243
|
+
|
|
2244
|
+
# Argumentos, entorno y navegador elegido que añade ZeroChat al arrancar un servicio gestionado,
|
|
2245
|
+
# calculados en cada arranque.
|
|
2246
|
+
SERVICE_LAUNCH = {"playwright": _playwright_launch}
|
|
2247
|
+
|
|
2248
|
+
|
|
2249
|
+
# Motores propios de Playwright: cada uno se compone de las entradas de la caché que instala
|
|
2250
|
+
# "playwright install <motor>"; ffmpeg es común y no se quita.
|
|
2251
|
+
ENGINE_CACHE_ENTRIES = {"chromium": ("chromium", "chromium-headless-shell"), "firefox": ("firefox",), "webkit": ("webkit",)}
|
|
2252
|
+
ENGINE_INSTALL_TIMEOUT_SECONDS = 1200
|
|
2253
|
+
_DRY_RUN_ENTRY = re.compile(r"^(.+?) \(playwright (\S+) v\d+\)\n\s+Install location:\s+(.+)$", re.MULTILINE)
|
|
2254
|
+
_INSTALL_PERCENT = re.compile(r"(\d{1,3})% of ")
|
|
2255
|
+
_INSTALL_DOWNLOAD = re.compile(r"^Downloading (.+?) \(playwright ")
|
|
2256
|
+
|
|
2257
|
+
|
|
2258
|
+
def playwright_engine_cache() -> Path:
|
|
2259
|
+
"""Caché de motores de Playwright sin lanzar Node: PLAYWRIGHT_BROWSERS_PATH o la ruta por defecto de la plataforma."""
|
|
2260
|
+
configured = os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "").strip()
|
|
2261
|
+
if configured:
|
|
2262
|
+
return Path(configured).expanduser()
|
|
2263
|
+
if sys.platform == "darwin":
|
|
2264
|
+
return Path.home() / "Library" / "Caches" / "ms-playwright"
|
|
2265
|
+
if sys.platform.startswith("win"):
|
|
2266
|
+
return Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local") / "ms-playwright"
|
|
2267
|
+
return Path.home() / ".cache" / "ms-playwright"
|
|
2268
|
+
|
|
2269
|
+
|
|
2270
|
+
def cached_engine_entries() -> set[str]:
|
|
2271
|
+
"""Entradas de ENGINE_CACHE_ENTRIES con alguna revisión completa en la caché; la revisión exacta de esta
|
|
2272
|
+
versión de Playwright solo la da BrowserEngines.list lanzando Node."""
|
|
2273
|
+
try:
|
|
2274
|
+
folders = [child.name for child in playwright_engine_cache().iterdir() if (child / "INSTALLATION_COMPLETE").is_file()]
|
|
2275
|
+
except OSError:
|
|
2276
|
+
return set()
|
|
2277
|
+
# Las carpetas se llaman como la entrada con guiones bajos y su revisión: chromium_headless_shell-1187.
|
|
2278
|
+
names = {name for entries in ENGINE_CACHE_ENTRIES.values() for name in entries}
|
|
2279
|
+
return {name for name in names if any(folder.startswith(name.replace("-", "_") + "-") for folder in folders)}
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def playwright_service_module() -> Path:
|
|
2283
|
+
return get_data_dir() / "services" / "playwright" / "node_modules" / "playwright"
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
def playwright_service_cli() -> Path:
|
|
2287
|
+
return playwright_service_module() / "cli.js"
|
|
2288
|
+
|
|
2289
|
+
|
|
2290
|
+
def system_browsers() -> dict[str, bool]:
|
|
2291
|
+
"""Chrome y Edge en las rutas donde los busca Playwright para --browser=chrome|msedge, Chromium donde lo
|
|
2292
|
+
busca chromium_install; Firefox solo se informa."""
|
|
2293
|
+
if sys.platform == "darwin":
|
|
2294
|
+
candidates = {
|
|
2295
|
+
"chrome": ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
|
|
2296
|
+
"msedge": ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
|
|
2297
|
+
"firefox": ["/Applications/Firefox.app/Contents/MacOS/firefox"],
|
|
2298
|
+
}
|
|
2299
|
+
elif sys.platform.startswith("win"):
|
|
2300
|
+
roots = [os.environ.get(name) for name in ("LOCALAPPDATA", "PROGRAMFILES", "PROGRAMFILES(X86)")]
|
|
2301
|
+
roots = [root for root in roots if root]
|
|
2302
|
+
candidates = {
|
|
2303
|
+
"chrome": [os.path.join(root, "Google", "Chrome", "Application", "chrome.exe") for root in roots],
|
|
2304
|
+
"msedge": [os.path.join(root, "Microsoft", "Edge", "Application", "msedge.exe") for root in roots],
|
|
2305
|
+
"firefox": [os.path.join(root, "Mozilla Firefox", "firefox.exe") for root in roots],
|
|
2306
|
+
}
|
|
2307
|
+
else:
|
|
2308
|
+
candidates = {
|
|
2309
|
+
"chrome": ["/opt/google/chrome/chrome"],
|
|
2310
|
+
"msedge": ["/opt/microsoft/msedge/msedge"],
|
|
2311
|
+
"firefox": [shutil.which("firefox") or ""],
|
|
2312
|
+
}
|
|
2313
|
+
found = {name: any(path and os.path.isfile(path) for path in paths) for name, paths in candidates.items()}
|
|
2314
|
+
return {**found, "chromium": chromium_install() is not None}
|
|
2315
|
+
|
|
2316
|
+
|
|
2317
|
+
def user_browser_data_dir(browser: str) -> Path | None:
|
|
2318
|
+
"""Carpeta de datos del usuario de Chrome, Edge o Chromium, donde están sus perfiles."""
|
|
2319
|
+
if browser == "chromium":
|
|
2320
|
+
install = chromium_install()
|
|
2321
|
+
return install[1] if install else None
|
|
2322
|
+
if sys.platform == "darwin":
|
|
2323
|
+
root, paths = Path.home() / "Library" / "Application Support", {"chrome": "Google/Chrome", "msedge": "Microsoft Edge"}
|
|
2324
|
+
elif sys.platform.startswith("win"):
|
|
2325
|
+
root = Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local")
|
|
2326
|
+
paths = {"chrome": "Google/Chrome/User Data", "msedge": "Microsoft/Edge/User Data"}
|
|
2327
|
+
else:
|
|
2328
|
+
root, paths = Path.home() / ".config", {"chrome": "google-chrome", "msedge": "microsoft-edge"}
|
|
2329
|
+
return root / paths[browser]
|
|
2330
|
+
|
|
2331
|
+
|
|
2332
|
+
def user_browser_extensions() -> dict[str, bool]:
|
|
2333
|
+
"""Si algún perfil de cada navegador del usuario tiene instalada la extensión de Playwright desde la tienda."""
|
|
2334
|
+
found = {}
|
|
2335
|
+
for browser in USER_BROWSERS:
|
|
2336
|
+
data_dir = user_browser_data_dir(browser)
|
|
2337
|
+
found[browser] = bool(data_dir) and any(data_dir.glob(f"*/Extensions/{PLAYWRIGHT_EXTENSION_ID}"))
|
|
2338
|
+
return found
|
|
2339
|
+
|
|
2340
|
+
|
|
2341
|
+
class BrowserEngines:
|
|
2342
|
+
"""Lista, instala y quita los motores propios de Playwright en su caché, nunca en el sistema."""
|
|
2343
|
+
|
|
2344
|
+
def __init__(self):
|
|
2345
|
+
self._lock = threading.Lock()
|
|
2346
|
+
self._jobs: dict[str, dict] = {}
|
|
2347
|
+
|
|
2348
|
+
@staticmethod
|
|
2349
|
+
def _run_cli(cli: Path, *args: str, timeout: float) -> subprocess.CompletedProcess:
|
|
2350
|
+
return subprocess.run([shutil.which("node") or "node", str(cli), *args], cwd=cli.parent,
|
|
2351
|
+
capture_output=True, text=True, timeout=timeout)
|
|
2352
|
+
|
|
2353
|
+
def _locations(self, cli: Path) -> dict[str, tuple[str, Path]]:
|
|
2354
|
+
"""Entradas de la caché de esta versión de Playwright: nombre -> (título con versión, ruta)."""
|
|
2355
|
+
names = [name for entries in ENGINE_CACHE_ENTRIES.values() for name in entries]
|
|
2356
|
+
result = self._run_cli(cli, "install", "--dry-run", *names, timeout=30)
|
|
2357
|
+
if result.returncode != 0:
|
|
2358
|
+
raise RuntimeError(f"Playwright no pudo listar sus navegadores: {(result.stderr or result.stdout).strip()}")
|
|
2359
|
+
return {name: (title, Path(location.strip())) for title, name, location in _DRY_RUN_ENTRY.findall(result.stdout)}
|
|
2360
|
+
|
|
2361
|
+
def list(self) -> dict:
|
|
2362
|
+
"""Navegadores del sistema y motores de Playwright; sin Playwright del servicio no hay motores."""
|
|
2363
|
+
cli = playwright_service_cli()
|
|
2364
|
+
result = {"systemBrowsers": system_browsers(), "extensions": user_browser_extensions(), "engines": []}
|
|
2365
|
+
if not cli.is_file():
|
|
2366
|
+
return result
|
|
2367
|
+
locations = self._locations(cli)
|
|
2368
|
+
with self._lock:
|
|
2369
|
+
jobs = {engine: dict(job) for engine, job in self._jobs.items()}
|
|
2370
|
+
for engine, entries in ENGINE_CACHE_ENTRIES.items():
|
|
2371
|
+
title = locations.get(entries[0], (engine,))[0]
|
|
2372
|
+
installed = all(name in locations and (locations[name][1] / "INSTALLATION_COMPLETE").is_file() for name in entries)
|
|
2373
|
+
result["engines"].append({"id": engine, "title": title, "installed": installed, **jobs.get(engine, {})})
|
|
2374
|
+
return result
|
|
2375
|
+
|
|
2376
|
+
def _check_engine(self, engine) -> Path:
|
|
2377
|
+
if engine not in ENGINE_CACHE_ENTRIES:
|
|
2378
|
+
raise ValueError(f"engine debe ser uno de: {', '.join(ENGINE_CACHE_ENTRIES)}")
|
|
2379
|
+
cli = playwright_service_cli()
|
|
2380
|
+
if not cli.is_file():
|
|
2381
|
+
raise ValueError("Playwright MCP no está instalado: arráncalo una vez desde Ajustes › MCP.")
|
|
2382
|
+
if self._jobs.get(engine, {}).get("status") == "installing":
|
|
2383
|
+
raise ValueError(f"{engine} ya se está instalando.")
|
|
2384
|
+
return cli
|
|
2385
|
+
|
|
2386
|
+
def install(self, engine) -> dict:
|
|
2387
|
+
with self._lock:
|
|
2388
|
+
cli = self._check_engine(engine)
|
|
2389
|
+
self._jobs[engine] = {"status": "installing", "percent": 0}
|
|
2390
|
+
threading.Thread(target=self._install, args=(cli, engine), daemon=True).start()
|
|
2391
|
+
return self.list()
|
|
2392
|
+
|
|
2393
|
+
def _install(self, cli: Path, engine: str):
|
|
2394
|
+
log_event(f"[zerochat] Instalando el motor {engine} de Playwright")
|
|
2395
|
+
lines = []
|
|
2396
|
+
try:
|
|
2397
|
+
proc = subprocess.Popen([shutil.which("node") or "node", str(cli), "install", engine], cwd=cli.parent,
|
|
2398
|
+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
2399
|
+
timer = threading.Timer(ENGINE_INSTALL_TIMEOUT_SECONDS, proc.kill)
|
|
2400
|
+
timer.start()
|
|
2401
|
+
try:
|
|
2402
|
+
for line in proc.stdout:
|
|
2403
|
+
lines = (lines + [line.rstrip()])[-20:]
|
|
2404
|
+
percent, download = _INSTALL_PERCENT.search(line), _INSTALL_DOWNLOAD.match(line)
|
|
2405
|
+
with self._lock:
|
|
2406
|
+
if download:
|
|
2407
|
+
self._jobs[engine].update(download=download.group(1), percent=0)
|
|
2408
|
+
elif percent:
|
|
2409
|
+
self._jobs[engine]["percent"] = min(100, int(percent.group(1)))
|
|
2410
|
+
code = proc.wait()
|
|
2411
|
+
finally:
|
|
2412
|
+
timer.cancel()
|
|
2413
|
+
if code != 0:
|
|
2414
|
+
raise RuntimeError("\n".join(lines[-5:]) or f"código {code}")
|
|
2415
|
+
with self._lock:
|
|
2416
|
+
self._jobs.pop(engine, None)
|
|
2417
|
+
log_event(f"[zerochat] Motor {engine} de Playwright instalado")
|
|
2418
|
+
except (OSError, RuntimeError, subprocess.SubprocessError) as err:
|
|
2419
|
+
with self._lock:
|
|
2420
|
+
self._jobs[engine] = {"status": "error", "error": format_log_error(str(err), 500)}
|
|
2421
|
+
log_event(f"[zerochat] Fallo instalando el motor {engine} de Playwright: {format_log_error(str(err))}")
|
|
2422
|
+
|
|
2423
|
+
def remove(self, engine) -> dict:
|
|
2424
|
+
with self._lock:
|
|
2425
|
+
cli = self._check_engine(engine)
|
|
2426
|
+
# Borrar un motor en uso deja al navegador abierto sin sus ficheros.
|
|
2427
|
+
if _playwright_active():
|
|
2428
|
+
raise ValueError("Detén Playwright MCP antes de quitar un motor.")
|
|
2429
|
+
locations = self._locations(cli)
|
|
2430
|
+
for name in ENGINE_CACHE_ENTRIES[engine]:
|
|
2431
|
+
if name in locations:
|
|
2432
|
+
shutil.rmtree(locations[name][1], ignore_errors=True)
|
|
2433
|
+
self._jobs.pop(engine, None)
|
|
2434
|
+
result = self.list()
|
|
2435
|
+
# La configuración no se queda con un motor quitado si hay otro instalado.
|
|
2436
|
+
config = load_browser_config()
|
|
2437
|
+
replacement = next((item["id"] for item in result["engines"] if item["installed"]), None)
|
|
2438
|
+
if config["engine"] == engine and replacement:
|
|
2439
|
+
save_browser_config({**config, "engine": replacement})
|
|
2440
|
+
return result
|
|
2441
|
+
|
|
2442
|
+
|
|
2443
|
+
BROWSER_ENGINES = BrowserEngines()
|
|
2116
2444
|
# ==============================================================================
|
|
2117
2445
|
# Servidor HTTP JSON-RPC 2.0 y SSE con Autenticación por Token
|
|
2118
2446
|
# ==============================================================================
|
|
@@ -2482,6 +2810,8 @@ class McpServiceManager:
|
|
|
2482
2810
|
self.clients: dict[str, StdioMcpClient] = {}
|
|
2483
2811
|
self.states: dict[str, str] = {}
|
|
2484
2812
|
self.errors: dict[str, str] = {}
|
|
2813
|
+
# Modo y navegador con los que arrancó cada servicio de SERVICE_LAUNCH.
|
|
2814
|
+
self.browsers: dict[str, dict] = {}
|
|
2485
2815
|
self._lock = threading.Lock()
|
|
2486
2816
|
materialize_managed_services(self.services_root)
|
|
2487
2817
|
self.services = self._load_services()
|
|
@@ -2532,6 +2862,12 @@ class McpServiceManager:
|
|
|
2532
2862
|
client = self.clients.get(server_id)
|
|
2533
2863
|
state = self.states.get(server_id, "stopped")
|
|
2534
2864
|
running = bool(client and client.running() and state == "running")
|
|
2865
|
+
error = self.errors.get(server_id)
|
|
2866
|
+
# El proceso de un servicio en marcha puede morir sin que nadie lo detenga.
|
|
2867
|
+
if state == "running" and client and not running:
|
|
2868
|
+
code = client.process.poll() if client.process else None
|
|
2869
|
+
state = "error"
|
|
2870
|
+
error = f"MCP process exited with code {code}" if code is not None else "MCP process is not responding"
|
|
2535
2871
|
pref = self.preferences.get(server_id, {})
|
|
2536
2872
|
result.append({
|
|
2537
2873
|
"id": server_id,
|
|
@@ -2542,10 +2878,11 @@ class McpServiceManager:
|
|
|
2542
2878
|
"enabled": pref.get("enabled", server.get("enabledByDefault", False)),
|
|
2543
2879
|
"status": "running" if running else state,
|
|
2544
2880
|
"toolCount": len(client.tools) if running else 0,
|
|
2545
|
-
"error":
|
|
2881
|
+
"error": error,
|
|
2546
2882
|
"options": server.get("options", []),
|
|
2547
2883
|
"userOptions": pref.get("options", {}),
|
|
2548
2884
|
"oauthUrl": client.oauth_url if client else None,
|
|
2885
|
+
"browser": self.browsers.get(server_id) if running else None,
|
|
2549
2886
|
})
|
|
2550
2887
|
|
|
2551
2888
|
return result
|
|
@@ -2652,7 +2989,9 @@ class McpServiceManager:
|
|
|
2652
2989
|
if int(version_match.group(1)) < required_node_major:
|
|
2653
2990
|
raise RuntimeError(f"{server_id} requiere Node.js {required_node_major}+; se detectó {detected}")
|
|
2654
2991
|
|
|
2655
|
-
def start(self, server_id: str) -> list[dict]:
|
|
2992
|
+
def start(self, server_id: str, remember: bool, browser: dict | None = None) -> list[dict]:
|
|
2993
|
+
"""Arranca un servicio; con remember lo guarda como activo para ofrecerlo en la siguiente sesión.
|
|
2994
|
+
`browser` es el navegador que exige el perfil activo, solo para Playwright MCP."""
|
|
2656
2995
|
with self._lock:
|
|
2657
2996
|
self.services = self._load_services()
|
|
2658
2997
|
server = self.services.get(server_id)
|
|
@@ -2682,10 +3021,14 @@ class McpServiceManager:
|
|
|
2682
3021
|
launch = server.get("launch", {})
|
|
2683
3022
|
command = self._expand(launch.get("executable", sys.executable), values)
|
|
2684
3023
|
args = [self._expand(arg, values) for arg in launch.get("args", []) + option_args]
|
|
2685
|
-
|
|
2686
3024
|
env = os.environ.copy()
|
|
2687
3025
|
for k, v in launch.get("env", {}).items():
|
|
2688
3026
|
env[k] = self._expand(v, values)
|
|
3027
|
+
self.browsers.pop(server_id, None)
|
|
3028
|
+
if server_id in SERVICE_LAUNCH:
|
|
3029
|
+
extra_args, extra_env, self.browsers[server_id] = SERVICE_LAUNCH[server_id](browser)
|
|
3030
|
+
args += extra_args
|
|
3031
|
+
env.update(extra_env)
|
|
2689
3032
|
|
|
2690
3033
|
client = StdioMcpClient(
|
|
2691
3034
|
command, args, str(service_dir), env,
|
|
@@ -2697,9 +3040,10 @@ class McpServiceManager:
|
|
|
2697
3040
|
self.clients[server_id] = client
|
|
2698
3041
|
client.start(int(launch.get("handshakeTimeoutSeconds", 15)))
|
|
2699
3042
|
self.states[server_id] = "running"
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
3043
|
+
if remember:
|
|
3044
|
+
entry = self.preferences.setdefault(server_id, {})
|
|
3045
|
+
entry["enabled"] = True
|
|
3046
|
+
self._save_preferences()
|
|
2703
3047
|
self._trace(server_id, "started successfully")
|
|
2704
3048
|
except Exception as exc:
|
|
2705
3049
|
self.states[server_id] = "error"
|
|
@@ -3228,12 +3572,16 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3228
3572
|
if is_heartbeat:
|
|
3229
3573
|
mark_browser_active()
|
|
3230
3574
|
# cwd permite a cada pestaña notar que otra ha cambiado el directorio del proyecto;
|
|
3231
|
-
# jobs, mostrar los comandos que siguen en marcha y avisar antes de cerrar si se detendrían
|
|
3575
|
+
# jobs, mostrar los comandos que siguen en marcha y avisar antes de cerrar si se detendrían;
|
|
3576
|
+
# playwright, si se puede navegar aunque se haya instalado o quitado durante la sesión;
|
|
3577
|
+
# servers, los servicios MCP externos aunque otra pestaña los cambie o su proceso muera.
|
|
3232
3578
|
self._send_json_response(200, {
|
|
3233
3579
|
"ok": True,
|
|
3234
3580
|
"cwd": str(Path.cwd().resolve()),
|
|
3235
3581
|
"jobs": [job.summary() for job in JOBS.running()],
|
|
3236
|
-
"exit_on_close": EXIT_ON_CLOSE
|
|
3582
|
+
"exit_on_close": EXIT_ON_CLOSE,
|
|
3583
|
+
"playwright": playwright_status(),
|
|
3584
|
+
"servers": GLOBAL_MCP_MANAGER.list_servers()
|
|
3237
3585
|
})
|
|
3238
3586
|
return
|
|
3239
3587
|
|
|
@@ -3257,7 +3605,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3257
3605
|
"version": VERSION,
|
|
3258
3606
|
"cwd": str(Path.cwd().resolve()),
|
|
3259
3607
|
"tools_count": len(LOCAL_TOOLS_DEFINITIONS),
|
|
3260
|
-
"browser_action": browser_action_availability(),
|
|
3261
3608
|
"os": DETECTED_OS
|
|
3262
3609
|
})
|
|
3263
3610
|
self._log_res(200, safe_path, (time.monotonic() - t0) * 1000)
|
|
@@ -3298,6 +3645,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3298
3645
|
self._reject(400, {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": f"Parse error: {err}"}},
|
|
3299
3646
|
safe_path, t0, f"Error parseando JSON: {err}")
|
|
3300
3647
|
return None
|
|
3648
|
+
# El tope ampliado de http_body_limit es solo para la exportación de --prompt --export.
|
|
3649
|
+
if content_len > MAX_HTTP_BODY_BYTES and not (isinstance(req, dict) and req.get("method") == "zerochat/run/export"):
|
|
3650
|
+
self._log_req("POST", safe_path)
|
|
3651
|
+
self._reject(413, {"error": "Request body too large"}, safe_path, t0, "Cuerpo HTTP excede el límite")
|
|
3652
|
+
return None
|
|
3301
3653
|
|
|
3302
3654
|
rpc_error = self._validate_rpc(req)
|
|
3303
3655
|
if rpc_error:
|
|
@@ -3386,18 +3738,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3386
3738
|
}
|
|
3387
3739
|
return self._initialize_result(server_info), None, ""
|
|
3388
3740
|
if method == "tools/list":
|
|
3389
|
-
|
|
3390
|
-
tools = []
|
|
3391
|
-
for definition in LOCAL_TOOLS_DEFINITIONS:
|
|
3392
|
-
item = dict(definition)
|
|
3393
|
-
if item.get("name") == "browser_action":
|
|
3394
|
-
item["availability"] = browser_availability
|
|
3395
|
-
tools.append(item)
|
|
3396
|
-
return {"tools": tools}, None, ""
|
|
3397
|
-
if method == "tools/availability":
|
|
3398
|
-
if params.get("name", "") != "browser_action":
|
|
3399
|
-
return None, {"code": -32602, "message": "Herramienta no compatible con comprobación de disponibilidad."}, ""
|
|
3400
|
-
return browser_action_availability(), None, ""
|
|
3741
|
+
return {"tools": [dict(definition) for definition in LOCAL_TOOLS_DEFINITIONS]}, None, ""
|
|
3401
3742
|
if method == "tools/call":
|
|
3402
3743
|
tool_name = params["name"]
|
|
3403
3744
|
handler = LOCAL_TOOL_HANDLERS.get(tool_name)
|
|
@@ -3444,10 +3785,10 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3444
3785
|
return self._dispatch_jobs(method, params)
|
|
3445
3786
|
if method.startswith("zerochat/run/"):
|
|
3446
3787
|
return dispatch_run(method, params)
|
|
3447
|
-
if method == "zerochat/external/status":
|
|
3448
|
-
return {"host": "running", "version": VERSION, "servers": GLOBAL_MCP_MANAGER.list_servers()}, None, ""
|
|
3449
3788
|
if method in ("zerochat/external/servers/start", "zerochat/external/servers/stop", "zerochat/external/servers/configure"):
|
|
3450
3789
|
return self._dispatch_server_control(method, params)
|
|
3790
|
+
if method.startswith("zerochat/browser/"):
|
|
3791
|
+
return self._dispatch_browser(method, params)
|
|
3451
3792
|
return None, {"code": -32601, "message": f"Método '{method}' no soportado."}, ""
|
|
3452
3793
|
|
|
3453
3794
|
@staticmethod
|
|
@@ -3479,17 +3820,49 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3479
3820
|
return job_details(job), None, ""
|
|
3480
3821
|
return None, {"code": -32601, "message": f"Método '{method}' no soportado."}, ""
|
|
3481
3822
|
|
|
3823
|
+
@staticmethod
|
|
3824
|
+
def _dispatch_browser(method: str, params: dict) -> tuple[dict | None, dict | None, str]:
|
|
3825
|
+
"""Configuración, datos y motores propios de Playwright para la sección Navegador de Ajustes."""
|
|
3826
|
+
try:
|
|
3827
|
+
if method == "zerochat/browser/config/get":
|
|
3828
|
+
return load_browser_config(), None, ""
|
|
3829
|
+
if method == "zerochat/browser/config/set":
|
|
3830
|
+
return save_browser_config(params.get("config")), None, ""
|
|
3831
|
+
if method == "zerochat/browser/data/clear":
|
|
3832
|
+
return clear_own_browser_data(params.get("engine")), None, ""
|
|
3833
|
+
if method == "zerochat/browser/engines/list":
|
|
3834
|
+
return BROWSER_ENGINES.list(), None, ""
|
|
3835
|
+
if method == "zerochat/browser/engines/install":
|
|
3836
|
+
return BROWSER_ENGINES.install(params.get("engine")), None, ""
|
|
3837
|
+
if method == "zerochat/browser/engines/remove":
|
|
3838
|
+
return BROWSER_ENGINES.remove(params.get("engine")), None, ""
|
|
3839
|
+
except (ValueError, RuntimeError, OSError, subprocess.SubprocessError) as exc:
|
|
3840
|
+
return None, {"code": -32602, "message": str(exc)}, ""
|
|
3841
|
+
return None, {"code": -32601, "message": f"Método '{method}' no soportado."}, ""
|
|
3842
|
+
|
|
3482
3843
|
@staticmethod
|
|
3483
3844
|
def _dispatch_server_control(method: str, params: dict) -> tuple[dict | None, dict | None, str]:
|
|
3484
3845
|
server_id = params.get("serverId")
|
|
3485
3846
|
opts = params.get("options", {})
|
|
3847
|
+
remember = params.get("remember")
|
|
3848
|
+
browser = params.get("browser")
|
|
3486
3849
|
if not isinstance(server_id, str) or not server_id:
|
|
3487
3850
|
return None, {"code": -32602, "message": "serverId debe ser un texto no vacío."}, ""
|
|
3488
3851
|
if not isinstance(opts, dict):
|
|
3489
3852
|
return None, {"code": -32602, "message": "options debe ser un objeto."}, ""
|
|
3853
|
+
# Al arrancar, el cliente decide siempre si el servicio se recuerda para la siguiente sesión.
|
|
3854
|
+
if method.endswith("/start") and not isinstance(remember, bool):
|
|
3855
|
+
return None, {"code": -32602, "message": "remember es obligatorio al arrancar y debe ser un booleano."}, ""
|
|
3856
|
+
if method.endswith("/start") and browser is not None:
|
|
3857
|
+
if server_id not in SERVICE_LAUNCH:
|
|
3858
|
+
return None, {"code": -32602, "message": f"{server_id} no admite elegir navegador."}, ""
|
|
3859
|
+
try:
|
|
3860
|
+
browser = validate_profile_browser(browser)
|
|
3861
|
+
except ValueError as exc:
|
|
3862
|
+
return None, {"code": -32602, "message": str(exc)}, ""
|
|
3490
3863
|
try:
|
|
3491
3864
|
if method.endswith("/start"):
|
|
3492
|
-
servers = GLOBAL_MCP_MANAGER.start(server_id)
|
|
3865
|
+
servers = GLOBAL_MCP_MANAGER.start(server_id, remember, browser)
|
|
3493
3866
|
elif method.endswith("/stop"):
|
|
3494
3867
|
servers = GLOBAL_MCP_MANAGER.stop(server_id)
|
|
3495
3868
|
else:
|
|
@@ -3807,7 +4180,7 @@ class RunSession:
|
|
|
3807
4180
|
allow_dangerous_tools: bool = False):
|
|
3808
4181
|
self.run_id = secrets.token_urlsafe(24)
|
|
3809
4182
|
self.prompt = prompt
|
|
3810
|
-
# Perfil
|
|
4183
|
+
# Perfil (nombre o id) que la interfaz usa solo en esta ejecución; None, el activo.
|
|
3811
4184
|
self.profile = profile
|
|
3812
4185
|
# Fichero donde se escribe la conversación exportada (formato por su extensión); None, sin exportar.
|
|
3813
4186
|
self.export_path = export_path
|
|
@@ -3837,7 +4210,8 @@ RUN_SESSION: RunSession | None = None
|
|
|
3837
4210
|
|
|
3838
4211
|
|
|
3839
4212
|
def http_body_limit() -> int:
|
|
3840
|
-
"""Tope del cuerpo
|
|
4213
|
+
"""Tope del cuerpo que se lee: el general, o el de la exportación mientras se espera (ff-server
|
|
4214
|
+
rechaza después cualquier otro método que supere el general)."""
|
|
3841
4215
|
session = RUN_SESSION
|
|
3842
4216
|
if session is not None and session.export_path is not None and not session.exported:
|
|
3843
4217
|
return RUN_EXPORT_MAX_BODY_BYTES
|
|
@@ -3997,7 +4371,7 @@ def main():
|
|
|
3997
4371
|
parser.add_argument("--no-venv", action="store_true", help="Omitir la comprobación/creación de ~/zerochat/.venv")
|
|
3998
4372
|
parser.add_argument("--test", action="store_true", help="Ejecutar autocomprobación interna de herramientas")
|
|
3999
4373
|
parser.add_argument("--prompt", default=None, help="Ejecutar este prompt en la interfaz, imprimir solo la respuesta final y terminar (salida 0 terminado, 1 error de generación, 2 plazo agotado o navegador cerrado, 3 fallo de arranque, 4 argumentos no válidos)")
|
|
4000
|
-
parser.add_argument("--profile", default=None, help="Con --prompt, perfil
|
|
4374
|
+
parser.add_argument("--profile", default=None, help="Con --prompt, perfil (nombre o id) que se usa solo en esta ejecución, sin cambiar el activo")
|
|
4001
4375
|
parser.add_argument("--log", choices=("on", "off"), default=None, help="Mostrar la cabecera y el registro del servidor (default: on; off con --prompt)")
|
|
4002
4376
|
parser.add_argument("--export", default=None, metavar="FICHERO", help=f"Con --prompt, guardar la conversación en FICHERO; el formato sale de la extensión ({', '.join(RUN_EXPORT_FORMATS)})")
|
|
4003
4377
|
parser.add_argument("--keep-page", action="store_true", help="Página: con --prompt, no cerrar la página al terminar para ver la conversación (no se combina con --headless)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|