zerochat 9.3.0__tar.gz → 9.4.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.3.0
3
+ Version: 9.4.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.3.0"
7
+ version = "9.4.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.3.0
3
+ Version: 9.4.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.3.0"
49
+ SOURCE_BACKEND_VERSION = "9.4.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,150 @@ def set_project_directory(path) -> dict:
1344
1344
  return {"cwd": str(target), "previous_cwd": previous}
1345
1345
 
1346
1346
 
1347
+ def open_file_diff(path: str, tool: str | None = None, commit_fallback: bool = False) -> dict:
1348
+ """Abre un visor externo de diffs (git difftool) para comparar el fichero actual frente a Git HEAD, su último commit o fichero vacío."""
1349
+ if not isinstance(path, str) or not path.strip():
1350
+ raise ValueError("La ruta del archivo está vacía.")
1351
+ if len(path) > MAX_PATH_LENGTH:
1352
+ raise ValueError("La ruta del archivo excede el tamaño máximo permitido.")
1353
+ if tool is not None:
1354
+ if not isinstance(tool, str) or not re.match(r"^[a-zA-Z0-9_-]{1,32}$", tool):
1355
+ raise ValueError("Nombre de herramienta de diff inválido.")
1356
+
1357
+ target = Path(path.strip()).expanduser().resolve()
1358
+ if not target.exists():
1359
+ raise ValueError(f"El archivo '{path}' no existe.")
1360
+ if not target.is_file():
1361
+ raise ValueError(f"'{path}' no es un archivo regular.")
1362
+
1363
+ git_bin = shutil.which("git")
1364
+ if not git_bin:
1365
+ raise RuntimeError("Git no está instalado o no se encuentra en el PATH del sistema.")
1366
+
1367
+ try:
1368
+ res = subprocess.run(
1369
+ [git_bin, "rev-parse", "--show-toplevel"],
1370
+ cwd=str(target.parent),
1371
+ capture_output=True,
1372
+ text=True,
1373
+ timeout=5,
1374
+ check=False
1375
+ )
1376
+ except Exception as err:
1377
+ raise RuntimeError(f"Error comprobando repositorio Git: {err}")
1378
+
1379
+ if res.returncode != 0:
1380
+ raise ValueError(f"El archivo '{target.name}' no pertenece a ningún repositorio Git.")
1381
+
1382
+ repo_root = Path(res.stdout.strip()).resolve()
1383
+
1384
+ has_head = subprocess.run(
1385
+ [git_bin, "rev-parse", "--verify", "HEAD"],
1386
+ cwd=str(repo_root),
1387
+ stdout=subprocess.DEVNULL,
1388
+ stderr=subprocess.DEVNULL,
1389
+ timeout=5,
1390
+ check=False
1391
+ ).returncode == 0
1392
+
1393
+ in_head = False
1394
+ has_working_tree_diff = True
1395
+ if has_head:
1396
+ try:
1397
+ rel_path = target.relative_to(repo_root).as_posix()
1398
+ in_head = subprocess.run(
1399
+ [git_bin, "cat-file", "-e", f"HEAD:{rel_path}"],
1400
+ cwd=str(repo_root),
1401
+ stdout=subprocess.DEVNULL,
1402
+ stderr=subprocess.DEVNULL,
1403
+ timeout=5,
1404
+ check=False
1405
+ ).returncode == 0
1406
+ if in_head:
1407
+ diff_check = subprocess.run(
1408
+ [git_bin, "diff", "--quiet", "HEAD", "--", str(target)],
1409
+ cwd=str(repo_root),
1410
+ timeout=5,
1411
+ check=False
1412
+ )
1413
+ has_working_tree_diff = (diff_check.returncode != 0)
1414
+ except ValueError:
1415
+ in_head = False
1416
+
1417
+ cmd = [git_bin, "difftool", "--no-prompt", "-y"]
1418
+ if tool:
1419
+ cmd.append(f"--tool={tool}")
1420
+ else:
1421
+ cmd.append("--gui")
1422
+
1423
+ mode = "working_tree"
1424
+ last_commit = ""
1425
+ if has_working_tree_diff:
1426
+ if in_head:
1427
+ cmd.extend(["HEAD", "--", str(target)])
1428
+ else:
1429
+ cmd.extend(["--no-index", "--", os.devnull, str(target)])
1430
+ else:
1431
+ # No hay cambios pendientes en el árbol de trabajo. Comprobar historial del archivo.
1432
+ rel_path = target.relative_to(repo_root).as_posix()
1433
+ log_res = subprocess.run(
1434
+ [git_bin, "log", "-n", "1", "--format=%H", "--", rel_path],
1435
+ cwd=str(repo_root),
1436
+ capture_output=True,
1437
+ text=True,
1438
+ timeout=5,
1439
+ check=False
1440
+ )
1441
+ last_commit = log_res.stdout.strip() if log_res.returncode == 0 else ""
1442
+ if not commit_fallback or not last_commit:
1443
+ return {
1444
+ "success": True,
1445
+ "has_diff": False,
1446
+ "in_head": in_head,
1447
+ "has_commit_history": bool(last_commit),
1448
+ "last_commit": last_commit or None,
1449
+ "path": str(target)
1450
+ }
1451
+
1452
+ mode = "commit"
1453
+ parent_check = subprocess.run(
1454
+ [git_bin, "rev-parse", "--verify", f"{last_commit}~1"],
1455
+ cwd=str(repo_root),
1456
+ stdout=subprocess.DEVNULL,
1457
+ stderr=subprocess.DEVNULL,
1458
+ timeout=5,
1459
+ check=False
1460
+ )
1461
+ if parent_check.returncode == 0:
1462
+ cmd.extend([f"{last_commit}~1", last_commit, "--", str(target)])
1463
+ else:
1464
+ cmd.extend(["4b825dc642cb6eb9a060e54bf8d69288fbee4904", last_commit, "--", str(target)])
1465
+
1466
+ popen_kwargs = {
1467
+ "cwd": str(repo_root),
1468
+ "stdout": subprocess.DEVNULL,
1469
+ "stderr": subprocess.DEVNULL,
1470
+ }
1471
+ if os.name == "nt":
1472
+ popen_kwargs["creationflags"] = (
1473
+ getattr(subprocess, "DETACHED_PROCESS", 0) |
1474
+ getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
1475
+ )
1476
+ else:
1477
+ popen_kwargs["start_new_session"] = True
1478
+
1479
+ proc = subprocess.Popen(cmd, **popen_kwargs)
1480
+ return {
1481
+ "success": True,
1482
+ "has_diff": True,
1483
+ "mode": mode,
1484
+ "pid": proc.pid,
1485
+ "path": str(target),
1486
+ "in_head": in_head,
1487
+ "commit": last_commit or None
1488
+ }
1489
+
1490
+
1347
1491
  def search_files(query: str, path: str = ".", file_pattern: str = None, max_results: int = 100) -> str:
1348
1492
  """Recursively search for plain text or regular expressions across project files."""
1349
1493
  try:
@@ -3286,6 +3430,14 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
3286
3430
  return set_project_directory(params.get("path")), None, ""
3287
3431
  except (ValueError, OSError) as exc:
3288
3432
  return None, {"code": -32602, "message": str(exc)}, ""
3433
+ if method == "zerochat/diff/open":
3434
+ try:
3435
+ commit_fallback = bool(params.get("commit_fallback") or params.get("commitFallback"))
3436
+ return open_file_diff(params.get("path"), params.get("tool"), commit_fallback), None, ""
3437
+ except (ValueError, RuntimeError) as exc:
3438
+ return None, {"code": -32602, "message": str(exc)}, ""
3439
+ except Exception as exc:
3440
+ return None, {"code": -32603, "message": f"Error al abrir el visor de diffs: {exc}"}, str(exc)
3289
3441
  if method == "zerochat/web/fetch":
3290
3442
  try:
3291
3443
  return fetch_web_resource(params), None, ""
File without changes
File without changes
File without changes