zerochat 9.8.0__tar.gz → 9.9.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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zerochat
3
- Version: 9.8.0
3
+ Version: 9.9.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "zerochat"
7
- version = "9.8.0"
7
+ version = "9.9.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.8.0
3
+ Version: 9.9.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
@@ -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.8.0"
49
+ SOURCE_BACKEND_VERSION = "9.9.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."""
@@ -1344,6 +1344,92 @@ def set_project_directory(path) -> dict:
1344
1344
  return {"cwd": str(target), "previous_cwd": previous}
1345
1345
 
1346
1346
 
1347
+ # Visores gráficos que `git difftool` ya sabe lanzar, en orden de preferencia, con el ejecutable que los delata.
1348
+ GUI_DIFF_TOOLS = {"vscode": "code", "meld": "meld", "kdiff3": "kdiff3", "kompare": "kompare", "gvimdiff": "gvim"}
1349
+ # Respaldo cuando no hay ninguno: visor web que se instala en el entorno aislado de ZeroChat.
1350
+ WEBDIFF_REQUIREMENT = "webdiff==1.4.0"
1351
+ WEBDIFF_INSTALL_TIMEOUT_SECONDS = 120
1352
+
1353
+
1354
+ def _venv_webdiff() -> Path:
1355
+ """Ruta de webdiff dentro de ~/zerochat/.venv, junto al intérprete del entorno."""
1356
+ venv_python = get_venv_python(get_venv_dir())
1357
+ return venv_python.with_name("webdiff" + venv_python.suffix)
1358
+
1359
+
1360
+ def _find_webdiff() -> str:
1361
+ """webdiff del entorno de ZeroChat o, si no, del PATH; '' si no hay ninguno."""
1362
+ venv_webdiff = _venv_webdiff()
1363
+ if venv_webdiff.exists():
1364
+ return str(venv_webdiff)
1365
+ return shutil.which("webdiff") or ""
1366
+
1367
+
1368
+ def _diff_tool_settings(tool: str, webdiff_bin: str) -> list:
1369
+ """Pares (clave, valor) de git config que deja `tool` como visor gráfico; webdiff no es un visor integrado de git."""
1370
+ settings = [("diff.guitool", tool)]
1371
+ if tool == "webdiff":
1372
+ settings.insert(0, ("difftool.webdiff.cmd", f'"{webdiff_bin}" "$LOCAL" "$REMOTE"'))
1373
+ return settings
1374
+
1375
+
1376
+ def detect_gui_diff_tool() -> dict:
1377
+ """Visor que proponer cuando Git no tiene ninguno: el primero instalado o, si no hay, webdiff.
1378
+
1379
+ Incluye las órdenes exactas que aplicará configure_gui_diff_tool, para mostrarlas antes de confirmar.
1380
+ """
1381
+ tool = next((name for name, binary in GUI_DIFF_TOOLS.items() if shutil.which(binary)), "webdiff")
1382
+ webdiff_bin = _find_webdiff()
1383
+ return {
1384
+ "tool": tool,
1385
+ "install": tool == "webdiff" and not webdiff_bin,
1386
+ "commands": [
1387
+ shlex.join(["git", "config", "--global", key, value])
1388
+ for key, value in _diff_tool_settings(tool, webdiff_bin or str(_venv_webdiff()))
1389
+ ],
1390
+ }
1391
+
1392
+
1393
+ def _install_webdiff() -> str:
1394
+ """Instala webdiff en ~/zerochat/.venv y devuelve la ruta del ejecutable."""
1395
+ ensure_virtual_environment()
1396
+ venv_python = get_venv_python(get_venv_dir())
1397
+ if not venv_python.exists():
1398
+ raise RuntimeError(f"No se pudo preparar el entorno virtual de ZeroChat: falta {venv_python}")
1399
+ res = subprocess.run(
1400
+ [str(venv_python), "-m", "pip", "install", WEBDIFF_REQUIREMENT],
1401
+ capture_output=True,
1402
+ text=True,
1403
+ timeout=WEBDIFF_INSTALL_TIMEOUT_SECONDS,
1404
+ check=False
1405
+ )
1406
+ if res.returncode != 0:
1407
+ raise RuntimeError(f"Error instalando webdiff: {(res.stderr or res.stdout).strip()[-500:]}")
1408
+ webdiff = _venv_webdiff()
1409
+ if not webdiff.exists():
1410
+ raise RuntimeError(f"webdiff se instaló pero no aparece en {webdiff}")
1411
+ return str(webdiff)
1412
+
1413
+
1414
+ def configure_gui_diff_tool(tool) -> dict:
1415
+ """Deja `tool` como visor gráfico global de Git, instalando antes webdiff si es el elegido y falta."""
1416
+ if not isinstance(tool, str) or (tool != "webdiff" and tool not in GUI_DIFF_TOOLS):
1417
+ raise ValueError(f"Visor de diffs '{tool}' no reconocido.")
1418
+ git_bin = shutil.which("git")
1419
+ if not git_bin:
1420
+ raise RuntimeError("Git no está instalado o no se encuentra en el PATH del sistema.")
1421
+
1422
+ webdiff_bin = ""
1423
+ if tool == "webdiff":
1424
+ webdiff_bin = _find_webdiff() or _install_webdiff()
1425
+ elif not shutil.which(GUI_DIFF_TOOLS[tool]):
1426
+ raise ValueError(f"'{GUI_DIFF_TOOLS[tool]}' no está en el PATH del sistema.")
1427
+
1428
+ for key, value in _diff_tool_settings(tool, webdiff_bin):
1429
+ subprocess.run([git_bin, "config", "--global", key, value], capture_output=True, timeout=5, check=True)
1430
+ return {"tool": tool}
1431
+
1432
+
1347
1433
  def _configured_gui_diff_tool(git_bin: str, repo_root: Path) -> str:
1348
1434
  """Herramienta gráfica que usaría `git difftool --gui`, o '' si no hay ninguna configurada.
1349
1435
 
@@ -1365,7 +1451,10 @@ def _configured_gui_diff_tool(git_bin: str, repo_root: Path) -> str:
1365
1451
 
1366
1452
 
1367
1453
  def open_file_diff(path: str, commit_fallback: bool = False) -> dict:
1368
- """Abre un visor externo de diffs (git difftool) para comparar el fichero actual frente a Git HEAD, su último commit o fichero vacío."""
1454
+ """Abre un visor externo de diffs (git difftool) para comparar el fichero actual frente a Git HEAD, su último commit o fichero vacío.
1455
+
1456
+ Sin visor gráfico configurado no lanza nada y devuelve en `setup` el que propone configurar.
1457
+ """
1369
1458
  if not isinstance(path, str) or not path.strip():
1370
1459
  raise ValueError("La ruta del archivo está vacía.")
1371
1460
  if len(path) > MAX_PATH_LENGTH:
@@ -1398,10 +1487,7 @@ def open_file_diff(path: str, commit_fallback: bool = False) -> dict:
1398
1487
 
1399
1488
  repo_root = Path(res.stdout.strip()).resolve()
1400
1489
  if not _configured_gui_diff_tool(git_bin, repo_root):
1401
- raise ValueError(
1402
- "No hay un visor gráfico de diffs configurado en Git. Configura uno, por ejemplo: "
1403
- "git config --global diff.guitool meld"
1404
- )
1490
+ return {"success": False, "setup": detect_gui_diff_tool()}
1405
1491
 
1406
1492
  has_head = subprocess.run(
1407
1493
  [git_bin, "rev-parse", "--verify", "HEAD"],
@@ -2218,18 +2304,22 @@ def playwright_launch_env(config: dict) -> dict[str, str]:
2218
2304
 
2219
2305
 
2220
2306
  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"]}
2307
+ """Navegador que exige un perfil: el propio con un motor, y headless False si exige la ventana
2308
+ visible, o el del usuario con uno de sus navegadores."""
2309
+ if isinstance(raw, dict) and raw.get("mode") == "own" and raw.get("engine") in OWN_BROWSER_ENGINES:
2310
+ if set(raw) == {"mode", "engine"}:
2311
+ return {"mode": "own", "engine": raw["engine"]}
2312
+ if set(raw) == {"mode", "engine", "headless"} and raw["headless"] is False:
2313
+ return {"mode": "own", "engine": raw["engine"], "headless": False}
2224
2314
  if isinstance(raw, dict) and raw.get("mode") == "user" and set(raw) == {"mode", "userBrowser"} and raw["userBrowser"] in USER_BROWSERS:
2225
2315
  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")
2316
+ raise ValueError("browser debe ser {mode: own, engine[, headless: false]} o {mode: user, userBrowser} con un motor o navegador admitido")
2227
2317
 
2228
2318
 
2229
2319
  def browser_choice(config: dict) -> dict:
2230
2320
  """Modo y navegador con los que arranca Playwright MCP, en la forma de validate_profile_browser."""
2231
2321
  if config["mode"] == "own":
2232
- return {"mode": "own", "engine": config["engine"]}
2322
+ return {"mode": "own", "engine": config["engine"], **({} if config["headless"] else {"headless": False})}
2233
2323
  return {"mode": "user", "userBrowser": config["userBrowser"]}
2234
2324
 
2235
2325
 
@@ -3774,6 +3864,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
3774
3864
  return None, {"code": -32602, "message": str(exc)}, ""
3775
3865
  except Exception as exc:
3776
3866
  return None, {"code": -32603, "message": f"Error al abrir el visor de diffs: {exc}"}, str(exc)
3867
+ if method == "zerochat/diff/configure":
3868
+ try:
3869
+ return configure_gui_diff_tool(params.get("tool")), None, ""
3870
+ except (ValueError, RuntimeError) as exc:
3871
+ return None, {"code": -32602, "message": str(exc)}, ""
3872
+ except Exception as exc:
3873
+ return None, {"code": -32603, "message": f"Error configurando el visor de diffs: {exc}"}, str(exc)
3777
3874
  if method == "zerochat/web/fetch":
3778
3875
  try:
3779
3876
  return fetch_web_resource(params), None, ""
File without changes
File without changes
File without changes