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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 9.0.0
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 **8.x**, que consolida las funcionalidades existentes. El desarrollo de nuevas funcionalidades queda en pausa por el momento.
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 **8.x** series, which consolidates existing features. New feature development is paused for now.
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, planes y puntos de control. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
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, plans and checkpoints. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
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 8.x — Español](https://albalday.github.io/zerochat/help/index.html)
97
- - [ZeroChat 8.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
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 **8.x**, que consolida las funcionalidades existentes. El desarrollo de nuevas funcionalidades queda en pausa por el momento.
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 **8.x** series, which consolidates existing features. New feature development is paused for now.
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, planes y puntos de control. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
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, plans and checkpoints. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
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 8.x — Español](https://albalday.github.io/zerochat/help/index.html)
75
- - [ZeroChat 8.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zerochat"
7
- version = "9.0.0"
7
+ version = "9.1.0"
8
8
  description = "Private local web client for AI models, tools and MCP services"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 9.0.0
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 **8.x**, que consolida las funcionalidades existentes. El desarrollo de nuevas funcionalidades queda en pausa por el momento.
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 **8.x** series, which consolidates existing features. New feature development is paused for now.
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, planes y puntos de control. El backend opcional añade archivos, comandos, diagnósticos, automatización de navegador y servicios MCP.
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, plans and checkpoints. The optional backend adds files, commands, diagnostics, browser automation and MCP services.
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 8.x — Español](https://albalday.github.io/zerochat/help/index.html)
97
- - [ZeroChat 8.x help — English](https://albalday.github.io/zerochat/help/en/index.html)
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.0.0"
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 Exception:
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 Exception:
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 Exception:
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 Exception:
303
- pass
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 Exception:
406
- # Modo offline o timeout ignorado de forma segura
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
- proc.kill()
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 Exception:
477
+ except (ProcessLookupError, PermissionError, OSError, AttributeError):
474
478
  try:
475
479
  proc.kill()
476
- except Exception:
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 the single occurrence of old_str with new_str in an existing file."""
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 Exception:
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 Exception:
1688
+ except (OSError, subprocess.SubprocessError):
1680
1689
  self._process.kill()
1681
- except Exception:
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": "Replace old_str with new_str in an existing file.",
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 antes de ejecutarlas."""
1911
- schemas = {
1912
- "list_directory": {
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
- expected = schema.get(name)
1963
- if expected is None:
1933
+ prop = properties.get(name)
1934
+ if prop is None:
1964
1935
  return f"Argumento no permitido: {name}"
1965
- expected_type, max_length = expected
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
- definition = next((item for item in LOCAL_TOOLS_DEFINITIONS if item["name"] == tool_name), {})
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 Exception:
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 Exception:
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 Exception:
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 Exception:
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 Exception:
2348
- pass
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 Exception:
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 Exception:
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 Exception:
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 Exception:
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