zerochat 9.0.0__tar.gz → 9.1.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.
- {zerochat-9.0.0/zerochat.egg-info → zerochat-9.1.0}/PKG-INFO +7 -7
- {zerochat-9.0.0 → zerochat-9.1.0}/README.md +6 -6
- {zerochat-9.0.0 → zerochat-9.1.0}/pyproject.toml +1 -1
- {zerochat-9.0.0 → zerochat-9.1.0/zerochat.egg-info}/PKG-INFO +7 -7
- {zerochat-9.0.0 → zerochat-9.1.0}/zerochat.py +76 -102
- {zerochat-9.0.0 → zerochat-9.1.0}/LICENSE +0 -0
- {zerochat-9.0.0 → zerochat-9.1.0}/setup.cfg +0 -0
- {zerochat-9.0.0 → zerochat-9.1.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-9.0.0 → zerochat-9.1.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-9.0.0 → zerochat-9.1.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-9.0.0 → zerochat-9.1.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.1.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
|
|
@@ -22,11 +22,11 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
# ZeroChat
|
|
24
24
|
|
|
25
|
-
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **
|
|
25
|
+
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **9.x**. Es un proyecto personal, de libre uso con licencia MIT si a alguien le sirve; su objetivo actual es servir de frontend único para un LLM local (Swift-Qwen3.8) con una API compatible con OpenAI.
|
|
26
26
|
|
|
27
27
|
La interfaz se sirve como archivos estáticos: `zerochat.html` necesita sus módulos `js/` y `css/`. El historial y la configuración se guardan en el navegador; no se sincronizan automáticamente entre dispositivos. Los proveedores remotos reciben el contenido incluido en las peticiones, y las herramientas web pueden utilizar servicios intermediarios. Almacenamiento local no significa privacidad absoluta.
|
|
28
28
|
|
|
29
|
-
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **
|
|
29
|
+
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **9.x** series. It is a personal project, free to use under the MIT licence if it helps anyone; its current goal is to serve as the single frontend for a local LLM (Swift-Qwen3.8) through an OpenAI-compatible API.
|
|
30
30
|
|
|
31
31
|
The interface is served as static files: `zerochat.html` needs its `js/` and `css/` modules. History and settings are stored in the browser, without automatic cross-device sync. Remote providers receive content included in requests, and web tools may use intermediary services. Local storage does not mean absolute privacy.
|
|
32
32
|
|
|
@@ -35,7 +35,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
35
35
|
- **Perfiles:** OpenAI y APIs de chat compatibles, Ollama, OpenRouter, Claude y Gemini. La visión, las herramientas y el razonamiento dependen del modelo y del adaptador.
|
|
36
36
|
- **WebLLM:** inferencia en el navegador con WebGPU, tras descargar un modelo. Necesita hardware compatible y memoria suficiente.
|
|
37
37
|
- **Documentos:** adjuntos y colecciones RAG con extracción local y búsqueda léxica mediante Orama; sin embeddings. Los fragmentos recuperados se envían al modelo elegido. La extracción de PDF tiene límites y no incluye OCR general.
|
|
38
|
-
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos,
|
|
38
|
+
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos, preguntas al usuario y subagentes de solo lectura. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
|
|
39
39
|
- **Historial y diagnóstico:** ramas de conversación, exportación JSON y Markdown, impresión a PDF, métricas de contexto y revisión de peticiones antes del envío.
|
|
40
40
|
|
|
41
41
|
*English:*
|
|
@@ -43,7 +43,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
43
43
|
- **Profiles:** OpenAI and compatible chat APIs, Ollama, OpenRouter, Claude and Gemini. Vision, tools and reasoning depend on the model and adapter.
|
|
44
44
|
- **WebLLM:** browser inference through WebGPU after downloading a model. Requires compatible hardware and enough memory.
|
|
45
45
|
- **Documents:** attachments and RAG collections with local extraction and Orama lexical search, without embeddings. Retrieved passages are sent to the selected model. PDF extraction has limitations and no general OCR.
|
|
46
|
-
- **Tools:** isolated JavaScript, web search and retrieval, charts,
|
|
46
|
+
- **Tools:** isolated JavaScript, web search and retrieval, charts, questions to the user and read-only subagents. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
|
|
47
47
|
- **History and diagnostics:** conversation branches, JSON and Markdown export, printing to PDF, context metrics and request inspection before sending.
|
|
48
48
|
|
|
49
49
|
## Inicio rápido / Quick start
|
|
@@ -93,8 +93,8 @@ Deleting `~/zerochat/` removes that state and managed MCP services, not the exec
|
|
|
93
93
|
|
|
94
94
|
## Documentación y ayuda / Documentation and help
|
|
95
95
|
|
|
96
|
-
- [Ayuda de ZeroChat
|
|
97
|
-
- [ZeroChat
|
|
96
|
+
- [Ayuda de ZeroChat 9.x — Español](https://albalday.github.io/zerochat/help/index.html)
|
|
97
|
+
- [ZeroChat 9.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
|
|
98
98
|
|
|
99
99
|
Las guías explican configuración, uso y límites.
|
|
100
100
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ZeroChat
|
|
2
2
|
|
|
3
|
-
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **
|
|
3
|
+
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **9.x**. Es un proyecto personal, de libre uso con licencia MIT si a alguien le sirve; su objetivo actual es servir de frontend único para un LLM local (Swift-Qwen3.8) con una API compatible con OpenAI.
|
|
4
4
|
|
|
5
5
|
La interfaz se sirve como archivos estáticos: `zerochat.html` necesita sus módulos `js/` y `css/`. El historial y la configuración se guardan en el navegador; no se sincronizan automáticamente entre dispositivos. Los proveedores remotos reciben el contenido incluido en las peticiones, y las herramientas web pueden utilizar servicios intermediarios. Almacenamiento local no significa privacidad absoluta.
|
|
6
6
|
|
|
7
|
-
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **
|
|
7
|
+
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **9.x** series. It is a personal project, free to use under the MIT licence if it helps anyone; its current goal is to serve as the single frontend for a local LLM (Swift-Qwen3.8) through an OpenAI-compatible API.
|
|
8
8
|
|
|
9
9
|
The interface is served as static files: `zerochat.html` needs its `js/` and `css/` modules. History and settings are stored in the browser, without automatic cross-device sync. Remote providers receive content included in requests, and web tools may use intermediary services. Local storage does not mean absolute privacy.
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
13
13
|
- **Perfiles:** OpenAI y APIs de chat compatibles, Ollama, OpenRouter, Claude y Gemini. La visión, las herramientas y el razonamiento dependen del modelo y del adaptador.
|
|
14
14
|
- **WebLLM:** inferencia en el navegador con WebGPU, tras descargar un modelo. Necesita hardware compatible y memoria suficiente.
|
|
15
15
|
- **Documentos:** adjuntos y colecciones RAG con extracción local y búsqueda léxica mediante Orama; sin embeddings. Los fragmentos recuperados se envían al modelo elegido. La extracción de PDF tiene límites y no incluye OCR general.
|
|
16
|
-
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos,
|
|
16
|
+
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos, preguntas al usuario y subagentes de solo lectura. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
|
|
17
17
|
- **Historial y diagnóstico:** ramas de conversación, exportación JSON y Markdown, impresión a PDF, métricas de contexto y revisión de peticiones antes del envío.
|
|
18
18
|
|
|
19
19
|
*English:*
|
|
@@ -21,7 +21,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
21
21
|
- **Profiles:** OpenAI and compatible chat APIs, Ollama, OpenRouter, Claude and Gemini. Vision, tools and reasoning depend on the model and adapter.
|
|
22
22
|
- **WebLLM:** browser inference through WebGPU after downloading a model. Requires compatible hardware and enough memory.
|
|
23
23
|
- **Documents:** attachments and RAG collections with local extraction and Orama lexical search, without embeddings. Retrieved passages are sent to the selected model. PDF extraction has limitations and no general OCR.
|
|
24
|
-
- **Tools:** isolated JavaScript, web search and retrieval, charts,
|
|
24
|
+
- **Tools:** isolated JavaScript, web search and retrieval, charts, questions to the user and read-only subagents. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
|
|
25
25
|
- **History and diagnostics:** conversation branches, JSON and Markdown export, printing to PDF, context metrics and request inspection before sending.
|
|
26
26
|
|
|
27
27
|
## Inicio rápido / Quick start
|
|
@@ -71,8 +71,8 @@ Deleting `~/zerochat/` removes that state and managed MCP services, not the exec
|
|
|
71
71
|
|
|
72
72
|
## Documentación y ayuda / Documentation and help
|
|
73
73
|
|
|
74
|
-
- [Ayuda de ZeroChat
|
|
75
|
-
- [ZeroChat
|
|
74
|
+
- [Ayuda de ZeroChat 9.x — Español](https://albalday.github.io/zerochat/help/index.html)
|
|
75
|
+
- [ZeroChat 9.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
|
|
76
76
|
|
|
77
77
|
Las guías explican configuración, uso y límites.
|
|
78
78
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zerochat
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.1.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
|
|
@@ -22,11 +22,11 @@ Dynamic: license-file
|
|
|
22
22
|
|
|
23
23
|
# ZeroChat
|
|
24
24
|
|
|
25
|
-
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **
|
|
25
|
+
ZeroChat es un cliente web de uso personal para conversar con modelos de IA, consultar documentos y utilizar herramientas. Esta documentación corresponde a la serie **9.x**. Es un proyecto personal, de libre uso con licencia MIT si a alguien le sirve; su objetivo actual es servir de frontend único para un LLM local (Swift-Qwen3.8) con una API compatible con OpenAI.
|
|
26
26
|
|
|
27
27
|
La interfaz se sirve como archivos estáticos: `zerochat.html` necesita sus módulos `js/` y `css/`. El historial y la configuración se guardan en el navegador; no se sincronizan automáticamente entre dispositivos. Los proveedores remotos reciben el contenido incluido en las peticiones, y las herramientas web pueden utilizar servicios intermediarios. Almacenamiento local no significa privacidad absoluta.
|
|
28
28
|
|
|
29
|
-
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **
|
|
29
|
+
*English:* ZeroChat is a web client for personal use: chatting with AI models, consulting documents and using tools. This documentation covers the **9.x** series. It is a personal project, free to use under the MIT licence if it helps anyone; its current goal is to serve as the single frontend for a local LLM (Swift-Qwen3.8) through an OpenAI-compatible API.
|
|
30
30
|
|
|
31
31
|
The interface is served as static files: `zerochat.html` needs its `js/` and `css/` modules. History and settings are stored in the browser, without automatic cross-device sync. Remote providers receive content included in requests, and web tools may use intermediary services. Local storage does not mean absolute privacy.
|
|
32
32
|
|
|
@@ -35,7 +35,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
35
35
|
- **Perfiles:** OpenAI y APIs de chat compatibles, Ollama, OpenRouter, Claude y Gemini. La visión, las herramientas y el razonamiento dependen del modelo y del adaptador.
|
|
36
36
|
- **WebLLM:** inferencia en el navegador con WebGPU, tras descargar un modelo. Necesita hardware compatible y memoria suficiente.
|
|
37
37
|
- **Documentos:** adjuntos y colecciones RAG con extracción local y búsqueda léxica mediante Orama; sin embeddings. Los fragmentos recuperados se envían al modelo elegido. La extracción de PDF tiene límites y no incluye OCR general.
|
|
38
|
-
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos,
|
|
38
|
+
- **Herramientas:** JavaScript aislado, búsqueda y lectura web, gráficos, preguntas al usuario y subagentes de solo lectura. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
|
|
39
39
|
- **Historial y diagnóstico:** ramas de conversación, exportación JSON y Markdown, impresión a PDF, métricas de contexto y revisión de peticiones antes del envío.
|
|
40
40
|
|
|
41
41
|
*English:*
|
|
@@ -43,7 +43,7 @@ The interface is served as static files: `zerochat.html` needs its `js/` and `cs
|
|
|
43
43
|
- **Profiles:** OpenAI and compatible chat APIs, Ollama, OpenRouter, Claude and Gemini. Vision, tools and reasoning depend on the model and adapter.
|
|
44
44
|
- **WebLLM:** browser inference through WebGPU after downloading a model. Requires compatible hardware and enough memory.
|
|
45
45
|
- **Documents:** attachments and RAG collections with local extraction and Orama lexical search, without embeddings. Retrieved passages are sent to the selected model. PDF extraction has limitations and no general OCR.
|
|
46
|
-
- **Tools:** isolated JavaScript, web search and retrieval, charts,
|
|
46
|
+
- **Tools:** isolated JavaScript, web search and retrieval, charts, questions to the user and read-only subagents. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
|
|
47
47
|
- **History and diagnostics:** conversation branches, JSON and Markdown export, printing to PDF, context metrics and request inspection before sending.
|
|
48
48
|
|
|
49
49
|
## Inicio rápido / Quick start
|
|
@@ -93,8 +93,8 @@ Deleting `~/zerochat/` removes that state and managed MCP services, not the exec
|
|
|
93
93
|
|
|
94
94
|
## Documentación y ayuda / Documentation and help
|
|
95
95
|
|
|
96
|
-
- [Ayuda de ZeroChat
|
|
97
|
-
- [ZeroChat
|
|
96
|
+
- [Ayuda de ZeroChat 9.x — Español](https://albalday.github.io/zerochat/help/index.html)
|
|
97
|
+
- [ZeroChat 9.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
|
|
98
98
|
|
|
99
99
|
Las guías explican configuración, uso y límites.
|
|
100
100
|
|
|
@@ -35,6 +35,7 @@ import tempfile
|
|
|
35
35
|
import threading
|
|
36
36
|
import time
|
|
37
37
|
import traceback
|
|
38
|
+
import urllib.error
|
|
38
39
|
import urllib.request
|
|
39
40
|
from urllib.parse import urlencode
|
|
40
41
|
import venv
|
|
@@ -42,7 +43,7 @@ import webbrowser
|
|
|
42
43
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
43
44
|
from pathlib import Path
|
|
44
45
|
|
|
45
|
-
SOURCE_BACKEND_VERSION = "9.
|
|
46
|
+
SOURCE_BACKEND_VERSION = "9.1.0"
|
|
46
47
|
|
|
47
48
|
def _read_source_version(filename: str) -> str | None:
|
|
48
49
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -53,7 +54,7 @@ def _read_source_version(filename: str) -> str | None:
|
|
|
53
54
|
match = re.search(r'version\s*=\s*"([0-9]+\.[0-9]+\.[0-9]+)"', content)
|
|
54
55
|
if match:
|
|
55
56
|
return match.group(1)
|
|
56
|
-
except
|
|
57
|
+
except (OSError, re.error):
|
|
57
58
|
pass
|
|
58
59
|
return None
|
|
59
60
|
|
|
@@ -77,7 +78,7 @@ def _read_ui_version() -> str:
|
|
|
77
78
|
data = json.loads(pkg_path.read_text(encoding="utf-8"))
|
|
78
79
|
if isinstance(data.get("version"), str):
|
|
79
80
|
return data["version"].strip()
|
|
80
|
-
except
|
|
81
|
+
except (OSError, json.JSONDecodeError):
|
|
81
82
|
pass
|
|
82
83
|
return BACKEND_PACKAGE_VERSION
|
|
83
84
|
|
|
@@ -294,13 +295,13 @@ def get_daily_token() -> str:
|
|
|
294
295
|
data = json.loads(token_file.read_text(encoding="utf-8"))
|
|
295
296
|
if data.get("date") == today and data.get("token") and isinstance(data["token"], str):
|
|
296
297
|
return data["token"]
|
|
297
|
-
except
|
|
298
|
+
except (OSError, json.JSONDecodeError):
|
|
298
299
|
pass
|
|
299
300
|
token = secrets.token_urlsafe(32)
|
|
300
301
|
try:
|
|
301
302
|
_write_private_text(token_file, json.dumps({"token": token, "date": today}, indent=2))
|
|
302
|
-
except
|
|
303
|
-
|
|
303
|
+
except OSError as err:
|
|
304
|
+
log_event(f"[zerochat] Advertencia: no se pudo guardar el token diario en {token_file}: {err}")
|
|
304
305
|
return token
|
|
305
306
|
|
|
306
307
|
|
|
@@ -402,8 +403,8 @@ def check_version():
|
|
|
402
403
|
else:
|
|
403
404
|
notice += f" Actualiza con: curl -sSL {REMOTE_SCRIPT_URL} -o zerochat.py"
|
|
404
405
|
add_notice(notice)
|
|
405
|
-
except
|
|
406
|
-
# Modo offline o
|
|
406
|
+
except (urllib.error.URLError, TimeoutError, OSError):
|
|
407
|
+
# Modo offline o sin conectividad al comprobar versiones remotas.
|
|
407
408
|
pass
|
|
408
409
|
# ==============================================================================
|
|
409
410
|
# Ejecución de comandos y tareas en segundo plano
|
|
@@ -465,15 +466,18 @@ def _kill_process_tree(proc: subprocess.Popen):
|
|
|
465
466
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
466
467
|
)
|
|
467
468
|
if proc.poll() is None:
|
|
468
|
-
|
|
469
|
+
try:
|
|
470
|
+
proc.kill()
|
|
471
|
+
except (ProcessLookupError, OSError, AttributeError):
|
|
472
|
+
pass
|
|
469
473
|
elif hasattr(os, "killpg") and hasattr(os, "getpgid"):
|
|
470
474
|
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
471
475
|
else:
|
|
472
476
|
proc.kill()
|
|
473
|
-
except
|
|
477
|
+
except (ProcessLookupError, PermissionError, OSError, AttributeError):
|
|
474
478
|
try:
|
|
475
479
|
proc.kill()
|
|
476
|
-
except
|
|
480
|
+
except (ProcessLookupError, OSError, AttributeError):
|
|
477
481
|
pass
|
|
478
482
|
|
|
479
483
|
|
|
@@ -1110,6 +1114,9 @@ def materialize_managed_services(services_root: Path | None = None) -> None:
|
|
|
1110
1114
|
# Herramientas Locales Core
|
|
1111
1115
|
# ==============================================================================
|
|
1112
1116
|
|
|
1117
|
+
MAX_HTTP_BODY_BYTES = 1024 * 1024
|
|
1118
|
+
MAX_PATH_LENGTH = 4096
|
|
1119
|
+
MAX_COMMAND_LENGTH = 16384
|
|
1113
1120
|
BROWSER_ACTIONS = ("navigate", "screenshot", "click", "fill")
|
|
1114
1121
|
|
|
1115
1122
|
|
|
@@ -1272,8 +1279,8 @@ def write_file(path: str, content: str) -> str:
|
|
|
1272
1279
|
return _tool_error(str(e))
|
|
1273
1280
|
|
|
1274
1281
|
|
|
1275
|
-
def edit_file(path: str, old_str: str, new_str: str) -> str:
|
|
1276
|
-
"""Replace
|
|
1282
|
+
def edit_file(path: str, old_str: str, new_str: str, replace_all: bool = False) -> str:
|
|
1283
|
+
"""Replace old_str with new_str in an existing file: its single occurrence, or all of them with replace_all."""
|
|
1277
1284
|
try:
|
|
1278
1285
|
target = Path(path).expanduser().resolve()
|
|
1279
1286
|
if not target.exists():
|
|
@@ -1284,19 +1291,21 @@ def edit_file(path: str, old_str: str, new_str: str) -> str:
|
|
|
1284
1291
|
with open(target, "r", encoding="utf-8", errors="replace") as f:
|
|
1285
1292
|
file_text = f.read()
|
|
1286
1293
|
|
|
1294
|
+
if not old_str:
|
|
1295
|
+
return _tool_error("old_str is empty. Provide the exact text to replace.")
|
|
1287
1296
|
occurrences = file_text.count(old_str)
|
|
1288
1297
|
if occurrences == 0:
|
|
1289
1298
|
return _tool_error(f"Target text was not found in '{path}'. Check exact file content with read_file.")
|
|
1290
|
-
if occurrences > 1:
|
|
1291
|
-
return _tool_error(f"Found {occurrences} matches for the snippet in '{path}'. Provide more surrounding context in old_str to ensure a unique match.")
|
|
1299
|
+
if occurrences > 1 and not replace_all:
|
|
1300
|
+
return _tool_error(f"Found {occurrences} matches for the snippet in '{path}'. Provide more surrounding context in old_str to ensure a unique match, or set replace_all to replace every match.")
|
|
1292
1301
|
|
|
1293
|
-
updated_text = file_text.replace(old_str, new_str, 1)
|
|
1302
|
+
updated_text = file_text.replace(old_str, new_str) if replace_all else file_text.replace(old_str, new_str, 1)
|
|
1294
1303
|
_atomic_write_text(target, updated_text)
|
|
1295
1304
|
return json.dumps({
|
|
1296
1305
|
"success": True,
|
|
1297
1306
|
"path": str(target),
|
|
1298
1307
|
"bytes_written": len(updated_text.encode("utf-8")),
|
|
1299
|
-
"replacements": 1
|
|
1308
|
+
"replacements": occurrences if replace_all else 1
|
|
1300
1309
|
}, ensure_ascii=False, indent=2)
|
|
1301
1310
|
except Exception as e:
|
|
1302
1311
|
return _tool_error(str(e))
|
|
@@ -1445,7 +1454,7 @@ def get_diagnostics(path: str | None = None) -> str:
|
|
|
1445
1454
|
"severity": "error",
|
|
1446
1455
|
"message": msg
|
|
1447
1456
|
})
|
|
1448
|
-
except
|
|
1457
|
+
except (subprocess.SubprocessError, OSError):
|
|
1449
1458
|
pass
|
|
1450
1459
|
elif ext == ".json":
|
|
1451
1460
|
try:
|
|
@@ -1676,9 +1685,9 @@ try {
|
|
|
1676
1685
|
self._process.stdin.write(json.dumps({"action": "close"}) + "\n")
|
|
1677
1686
|
self._process.stdin.flush()
|
|
1678
1687
|
self._process.wait(timeout=2)
|
|
1679
|
-
except
|
|
1688
|
+
except (OSError, subprocess.SubprocessError):
|
|
1680
1689
|
self._process.kill()
|
|
1681
|
-
except
|
|
1690
|
+
except (OSError, subprocess.SubprocessError):
|
|
1682
1691
|
pass
|
|
1683
1692
|
self._process = None
|
|
1684
1693
|
|
|
@@ -1746,7 +1755,7 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1746
1755
|
"inputSchema": {
|
|
1747
1756
|
"type": "object",
|
|
1748
1757
|
"properties": {
|
|
1749
|
-
"path": {"type": "string", "description": "Relative or absolute directory path (defaults to '.')"},
|
|
1758
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "Relative or absolute directory path (defaults to '.')"},
|
|
1750
1759
|
"recursive": {"type": "boolean", "description": "If true, traverses subdirectories up to depth 3", "default": False}
|
|
1751
1760
|
}
|
|
1752
1761
|
}
|
|
@@ -1757,7 +1766,7 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1757
1766
|
"inputSchema": {
|
|
1758
1767
|
"type": "object",
|
|
1759
1768
|
"properties": {
|
|
1760
|
-
"path": {"type": "string", "description": "File path"},
|
|
1769
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "File path"},
|
|
1761
1770
|
"start_line": {"type": "integer", "description": "First line (1-based)", "default": 1},
|
|
1762
1771
|
"end_line": {"type": "integer", "description": "Last line (inclusive)"},
|
|
1763
1772
|
"max_lines": {"type": "integer", "description": "Line cap if no end_line", "default": 500},
|
|
@@ -1772,21 +1781,26 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1772
1781
|
"inputSchema": {
|
|
1773
1782
|
"type": "object",
|
|
1774
1783
|
"properties": {
|
|
1775
|
-
"path": {"type": "string", "description": "Path to the file to write"},
|
|
1776
|
-
"content": {"type": "string", "description": "Full content of the file"}
|
|
1784
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "Path to the file to write"},
|
|
1785
|
+
"content": {"type": "string", "maxLength": MAX_HTTP_BODY_BYTES, "description": "Full content of the file"}
|
|
1777
1786
|
},
|
|
1778
1787
|
"required": ["path", "content"]
|
|
1779
1788
|
}
|
|
1780
1789
|
},
|
|
1781
1790
|
{
|
|
1782
1791
|
"name": "edit_file",
|
|
1783
|
-
"description":
|
|
1792
|
+
"description": (
|
|
1793
|
+
"Replace old_str with new_str in an existing file. "
|
|
1794
|
+
"For several changes, in this file or in others, emit several edit_file calls in the same response; "
|
|
1795
|
+
"they are applied in order, each one on the result of the previous one."
|
|
1796
|
+
),
|
|
1784
1797
|
"inputSchema": {
|
|
1785
1798
|
"type": "object",
|
|
1786
1799
|
"properties": {
|
|
1787
|
-
"path": {"type": "string", "description": "File path"},
|
|
1788
|
-
"old_str": {"type": "string", "description": "Exact text to replace; must occur once"},
|
|
1789
|
-
"new_str": {"type": "string", "description": "Replacement text"}
|
|
1800
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "File path"},
|
|
1801
|
+
"old_str": {"type": "string", "maxLength": MAX_HTTP_BODY_BYTES, "description": "Exact text to replace; must occur once unless replace_all is true"},
|
|
1802
|
+
"new_str": {"type": "string", "maxLength": MAX_HTTP_BODY_BYTES, "description": "Replacement text"},
|
|
1803
|
+
"replace_all": {"type": "boolean", "description": "Replace every occurrence of old_str (e.g. to rename an identifier)", "default": False}
|
|
1790
1804
|
},
|
|
1791
1805
|
"required": ["path", "old_str", "new_str"]
|
|
1792
1806
|
}
|
|
@@ -1797,9 +1811,9 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1797
1811
|
"inputSchema": {
|
|
1798
1812
|
"type": "object",
|
|
1799
1813
|
"properties": {
|
|
1800
|
-
"query": {"type": "string", "description": "Plain text string or regular expression to search for"},
|
|
1801
|
-
"path": {"type": "string", "description": "Base search directory (defaults to '.')", "default": "."},
|
|
1802
|
-
"file_pattern": {"type": "string", "description": "Optional glob filter (e.g. *.js, *.py)"}
|
|
1814
|
+
"query": {"type": "string", "maxLength": 4096, "description": "Plain text string or regular expression to search for"},
|
|
1815
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "Base search directory (defaults to '.')", "default": "."},
|
|
1816
|
+
"file_pattern": {"type": "string", "maxLength": 256, "description": "Optional glob filter (e.g. *.js, *.py)"}
|
|
1803
1817
|
},
|
|
1804
1818
|
"required": ["query"]
|
|
1805
1819
|
}
|
|
@@ -1825,6 +1839,7 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1825
1839
|
"properties": {
|
|
1826
1840
|
"command": {
|
|
1827
1841
|
"type": "string",
|
|
1842
|
+
"maxLength": MAX_COMMAND_LENGTH,
|
|
1828
1843
|
"description": (
|
|
1829
1844
|
"PowerShell command to run (e.g. git status; npm test)"
|
|
1830
1845
|
if DETECTED_OS == "windows"
|
|
@@ -1856,7 +1871,7 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1856
1871
|
"inputSchema": {
|
|
1857
1872
|
"type": "object",
|
|
1858
1873
|
"properties": {
|
|
1859
|
-
"path": {"type": "string", "description": "Path to the file or directory to inspect (defaults to '.')"}
|
|
1874
|
+
"path": {"type": "string", "maxLength": MAX_PATH_LENGTH, "description": "Path to the file or directory to inspect (defaults to '.')"}
|
|
1860
1875
|
}
|
|
1861
1876
|
}
|
|
1862
1877
|
},
|
|
@@ -1869,11 +1884,12 @@ LOCAL_TOOLS_DEFINITIONS = [
|
|
|
1869
1884
|
"action": {
|
|
1870
1885
|
"type": "string",
|
|
1871
1886
|
"enum": list(BROWSER_ACTIONS),
|
|
1887
|
+
"maxLength": 32,
|
|
1872
1888
|
"description": "Action to perform in the browser"
|
|
1873
1889
|
},
|
|
1874
|
-
"url": {"type": "string", "description": "Target URL for navigate or screenshot"},
|
|
1875
|
-
"selector": {"type": "string", "description": "CSS selector for click or fill actions"},
|
|
1876
|
-
"value": {"type": "string", "description": "Text value to type for fill action"}
|
|
1890
|
+
"url": {"type": "string", "maxLength": 4096, "description": "Target URL for navigate or screenshot"},
|
|
1891
|
+
"selector": {"type": "string", "maxLength": 1024, "description": "CSS selector for click or fill actions"},
|
|
1892
|
+
"value": {"type": "string", "maxLength": MAX_HTTP_BODY_BYTES, "description": "Text value to type for fill action"}
|
|
1877
1893
|
},
|
|
1878
1894
|
"required": ["action"]
|
|
1879
1895
|
}
|
|
@@ -1895,11 +1911,8 @@ LOCAL_TOOL_HANDLERS = {
|
|
|
1895
1911
|
# Servidor HTTP JSON-RPC 2.0 y SSE con Autenticación por Token
|
|
1896
1912
|
# ==============================================================================
|
|
1897
1913
|
|
|
1898
|
-
MAX_HTTP_BODY_BYTES = 1024 * 1024
|
|
1899
1914
|
MAX_RPC_METHOD_LENGTH = 128
|
|
1900
1915
|
MAX_TOOL_NAME_LENGTH = 128
|
|
1901
|
-
MAX_PATH_LENGTH = 4096
|
|
1902
|
-
MAX_COMMAND_LENGTH = 16384
|
|
1903
1916
|
# Un mensaje MCP admite imágenes en base64, pero no puede agotar la memoria del backend.
|
|
1904
1917
|
MAX_MCP_MESSAGE_CHARS = 16 * 1024 * 1024
|
|
1905
1918
|
MAX_MCP_STDERR_LINE_CHARS = 64 * 1024
|
|
@@ -1907,73 +1920,32 @@ DEFAULT_MCP_CALL_TIMEOUT_SECONDS = 30
|
|
|
1907
1920
|
|
|
1908
1921
|
|
|
1909
1922
|
def validate_local_tool_arguments(tool_name: str, arguments: dict) -> str | None:
|
|
1910
|
-
"""Valida tipos y límites de las herramientas locales
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
"path": (str, MAX_PATH_LENGTH),
|
|
1914
|
-
"recursive": (bool, None)
|
|
1915
|
-
},
|
|
1916
|
-
"read_file": {
|
|
1917
|
-
"path": (str, MAX_PATH_LENGTH),
|
|
1918
|
-
"start_line": (int, None),
|
|
1919
|
-
"end_line": (int, None),
|
|
1920
|
-
"max_lines": (int, None),
|
|
1921
|
-
"max_bytes": (int, None)
|
|
1922
|
-
},
|
|
1923
|
-
"write_file": {
|
|
1924
|
-
"path": (str, MAX_PATH_LENGTH),
|
|
1925
|
-
"content": (str, MAX_HTTP_BODY_BYTES)
|
|
1926
|
-
},
|
|
1927
|
-
"edit_file": {
|
|
1928
|
-
"path": (str, MAX_PATH_LENGTH),
|
|
1929
|
-
"old_str": (str, MAX_HTTP_BODY_BYTES),
|
|
1930
|
-
"new_str": (str, MAX_HTTP_BODY_BYTES)
|
|
1931
|
-
},
|
|
1932
|
-
"run_command": {
|
|
1933
|
-
"command": (str, MAX_COMMAND_LENGTH),
|
|
1934
|
-
"timeout_seconds": (int, None),
|
|
1935
|
-
"background": (bool, None)
|
|
1936
|
-
},
|
|
1937
|
-
"check_command": {
|
|
1938
|
-
"job_id": (int, None),
|
|
1939
|
-
"wait_seconds": (int, None),
|
|
1940
|
-
"stop": (bool, None)
|
|
1941
|
-
},
|
|
1942
|
-
"search_files": {
|
|
1943
|
-
"query": (str, 4096),
|
|
1944
|
-
"path": (str, MAX_PATH_LENGTH),
|
|
1945
|
-
"file_pattern": (str, 256)
|
|
1946
|
-
},
|
|
1947
|
-
"get_diagnostics": {
|
|
1948
|
-
"path": (str, MAX_PATH_LENGTH)
|
|
1949
|
-
},
|
|
1950
|
-
"browser_action": {
|
|
1951
|
-
"action": (str, 32),
|
|
1952
|
-
"url": (str, 4096),
|
|
1953
|
-
"selector": (str, 1024),
|
|
1954
|
-
"value": (str, MAX_HTTP_BODY_BYTES)
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
schema = schemas.get(tool_name)
|
|
1958
|
-
if schema is None:
|
|
1923
|
+
"""Valida tipos y límites de las herramientas locales a partir de su definición canónica."""
|
|
1924
|
+
definition = next((item for item in LOCAL_TOOLS_DEFINITIONS if item["name"] == tool_name), None)
|
|
1925
|
+
if definition is None:
|
|
1959
1926
|
return None
|
|
1960
1927
|
|
|
1928
|
+
schema = definition.get("inputSchema", {})
|
|
1929
|
+
properties = schema.get("properties", {})
|
|
1930
|
+
type_map = {"string": str, "integer": int, "boolean": bool}
|
|
1931
|
+
|
|
1961
1932
|
for name, value in arguments.items():
|
|
1962
|
-
|
|
1963
|
-
if
|
|
1933
|
+
prop = properties.get(name)
|
|
1934
|
+
if prop is None:
|
|
1964
1935
|
return f"Argumento no permitido: {name}"
|
|
1965
|
-
|
|
1936
|
+
expected_type_name = prop.get("type")
|
|
1937
|
+
expected_type = type_map.get(expected_type_name)
|
|
1966
1938
|
if expected_type is bool:
|
|
1967
1939
|
if not isinstance(value, bool):
|
|
1968
1940
|
return f"Tipo inválido para '{name}'"
|
|
1969
1941
|
else:
|
|
1970
1942
|
if isinstance(value, bool) or not isinstance(value, expected_type):
|
|
1971
1943
|
return f"Tipo inválido para '{name}'"
|
|
1944
|
+
max_length = prop.get("maxLength")
|
|
1972
1945
|
if max_length is not None and len(value) > max_length:
|
|
1973
1946
|
return f"'{name}' excede el tamaño máximo permitido"
|
|
1974
1947
|
|
|
1975
|
-
|
|
1976
|
-
for name in definition.get("inputSchema", {}).get("required", ()):
|
|
1948
|
+
for name in schema.get("required", ()):
|
|
1977
1949
|
if name not in arguments:
|
|
1978
1950
|
return f"Falta el argumento obligatorio: {name}"
|
|
1979
1951
|
|
|
@@ -2173,7 +2145,7 @@ class StdioMcpClient:
|
|
|
2173
2145
|
continue
|
|
2174
2146
|
try:
|
|
2175
2147
|
msg = json.loads(line)
|
|
2176
|
-
except
|
|
2148
|
+
except json.JSONDecodeError:
|
|
2177
2149
|
continue
|
|
2178
2150
|
req_id = msg.get("id")
|
|
2179
2151
|
if req_id is not None:
|
|
@@ -2247,7 +2219,7 @@ class StdioMcpClient:
|
|
|
2247
2219
|
if self.running():
|
|
2248
2220
|
try:
|
|
2249
2221
|
self._write(json.dumps({"jsonrpc": "2.0", "method": method}) + "\n")
|
|
2250
|
-
except
|
|
2222
|
+
except (OSError, ValueError):
|
|
2251
2223
|
self._alive = False
|
|
2252
2224
|
|
|
2253
2225
|
def _signal_group(self, sig) -> bool:
|
|
@@ -2327,7 +2299,7 @@ class McpServiceManager:
|
|
|
2327
2299
|
server["id"] = server_id
|
|
2328
2300
|
server["_directory"] = directory
|
|
2329
2301
|
servers[server_id] = server
|
|
2330
|
-
except
|
|
2302
|
+
except (OSError, json.JSONDecodeError):
|
|
2331
2303
|
continue
|
|
2332
2304
|
return servers
|
|
2333
2305
|
|
|
@@ -2335,7 +2307,7 @@ class McpServiceManager:
|
|
|
2335
2307
|
if self.config_file.exists():
|
|
2336
2308
|
try:
|
|
2337
2309
|
return json.loads(self.config_file.read_text(encoding="utf-8"))
|
|
2338
|
-
except
|
|
2310
|
+
except (OSError, json.JSONDecodeError):
|
|
2339
2311
|
return {}
|
|
2340
2312
|
return {}
|
|
2341
2313
|
|
|
@@ -2344,8 +2316,8 @@ class McpServiceManager:
|
|
|
2344
2316
|
tmp = self.config_file.with_suffix(".tmp")
|
|
2345
2317
|
tmp.write_text(json.dumps(self.preferences, indent=2), encoding="utf-8")
|
|
2346
2318
|
tmp.replace(self.config_file)
|
|
2347
|
-
except
|
|
2348
|
-
|
|
2319
|
+
except OSError as err:
|
|
2320
|
+
log_event(f"[zerochat] Advertencia al guardar preferencias MCP en {self.config_file}: {err}")
|
|
2349
2321
|
|
|
2350
2322
|
def list_servers(self) -> list[dict]:
|
|
2351
2323
|
self.services = self._load_services()
|
|
@@ -2412,7 +2384,7 @@ class McpServiceManager:
|
|
|
2412
2384
|
installation = json.loads(marker.read_text(encoding="utf-8"))
|
|
2413
2385
|
if installation.get("package") != package or installation.get("version") != version:
|
|
2414
2386
|
needs_install = True
|
|
2415
|
-
except
|
|
2387
|
+
except (OSError, json.JSONDecodeError):
|
|
2416
2388
|
needs_install = True
|
|
2417
2389
|
|
|
2418
2390
|
if needs_install:
|
|
@@ -2596,7 +2568,7 @@ class McpServiceManager:
|
|
|
2596
2568
|
for t in client.tools:
|
|
2597
2569
|
try:
|
|
2598
2570
|
matches = public_tool_name(server_id, t.get("name")) == public_name
|
|
2599
|
-
except
|
|
2571
|
+
except (ValueError, TypeError):
|
|
2600
2572
|
continue
|
|
2601
2573
|
if matches:
|
|
2602
2574
|
target = (client, t["name"])
|
|
@@ -2957,7 +2929,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2957
2929
|
if content_len <= MAX_HTTP_BODY_BYTES * 2:
|
|
2958
2930
|
try:
|
|
2959
2931
|
self.rfile.read(content_len)
|
|
2960
|
-
except
|
|
2932
|
+
except (OSError, ValueError):
|
|
2961
2933
|
pass
|
|
2962
2934
|
self.close_connection = True
|
|
2963
2935
|
self._reject(413, {"error": "Request body too large"}, safe_path, t0, "Cuerpo HTTP excede el límite")
|
|
@@ -3090,7 +3062,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
3090
3062
|
parsed_out = json.loads(tool_output_json)
|
|
3091
3063
|
if isinstance(parsed_out, dict) and parsed_out.get("success") is False:
|
|
3092
3064
|
tool_error_info = str(parsed_out.get("error", "Error en herramienta local"))
|
|
3093
|
-
except
|
|
3065
|
+
except (json.JSONDecodeError, TypeError):
|
|
3094
3066
|
pass
|
|
3095
3067
|
return {"content": [{"type": "text", "text": tool_output_json}], "isError": bool(tool_error_info)}, None, tool_error_info
|
|
3096
3068
|
if method == "zerochat/project/set_directory":
|
|
@@ -3410,6 +3382,8 @@ def main():
|
|
|
3410
3382
|
global TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES
|
|
3411
3383
|
|
|
3412
3384
|
parser = argparse.ArgumentParser(description=f"ZeroChat Local Server v{VERSION}")
|
|
3385
|
+
# Desde Python 3.14 argparse colorea la ayuda con códigos ANSI; la consola la muestra en texto plano.
|
|
3386
|
+
parser.color = False
|
|
3413
3387
|
parser.add_argument("--port", type=int, default=int(os.environ.get("ZEROCHAT_PORT", DEFAULT_PORT)), help=f"Puerto de escucha (default: {DEFAULT_PORT})")
|
|
3414
3388
|
parser.add_argument("--host", default=os.environ.get("ZEROCHAT_HOST", DEFAULT_HOST), help=f"Host de escucha (default: {DEFAULT_HOST})")
|
|
3415
3389
|
parser.add_argument("--ui-url", default=None, help="URL de la interfaz web a abrir (por defecto: interfaz local en desarrollo o GitHub Pages)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|