diff --git a/tools/local_screenshot_bridge.py b/tools/local_screenshot_bridge.py
index defce21..92706d6 100755
--- a/tools/local_screenshot_bridge.py
+++ b/tools/local_screenshot_bridge.py
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import argparse
import base64
+import io
import json
import os
import re
@@ -8,10 +9,12 @@ import subprocess
import sys
import time
import traceback
+import zipfile
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import Optional
+from urllib.parse import urlparse
_LOG_LEVELS = {"debug": 10, "info": 20, "error": 40, "quiet": 100}
@@ -585,20 +588,172 @@ class Handler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body)
+ def _send_html(self, status: int, html: str):
+ body = html.encode("utf-8")
+ self.send_response(status)
+ self.send_header("Content-Type", "text/html; charset=utf-8")
+ self.send_header("Content-Length", str(len(body)))
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.end_headers()
+ self.wfile.write(body)
+
+ def _send_bytes(self, status: int, body: bytes, content_type: str, content_disposition: Optional[str] = None):
+ self.send_response(status)
+ self.send_header("Content-Type", content_type)
+ self.send_header("Content-Length", str(len(body)))
+ self.send_header("Access-Control-Allow-Origin", "*")
+ if content_disposition:
+ self.send_header("Content-Disposition", content_disposition)
+ self.end_headers()
+ self.wfile.write(body)
+
+ def _build_extension_zip(self) -> tuple[Optional[bytes], Optional[str]]:
+ project_root: Path = self.server.project_root # type: ignore[attr-defined]
+ ext_dir = project_root / "chrome_screenshot_ext"
+ if not ext_dir.exists() or not ext_dir.is_dir():
+ return None, f"missing extension directory: {ext_dir}"
+
+ buf = io.BytesIO()
+ with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
+ for p in sorted(ext_dir.rglob("*")):
+ if p.is_file():
+ zf.write(p, arcname=p.relative_to(project_root))
+ return buf.getvalue(), None
+
+ def _health_payload(self) -> dict:
+ return {
+ "ok": True,
+ "service": "local_screenshot_bridge",
+ "out_dir": str(self.server.out_dir), # type: ignore[attr-defined]
+ "has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined]
+ "ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined]
+ }
+
+ def _home_html(self) -> str:
+ health = self._health_payload()
+ ai_text = "Enabled" if health["ai_enabled"] else "Disabled"
+ run_cmd_text = "Configured" if health["has_run_cmd"] else "Not configured"
+ return f"""
+
+
+
+
+ Local Screenshot Bridge
+
+
+
+
+
+ ✅ Screenshot Bridge is running
+ This server accepts screenshot payloads from the Chrome extension and can generate AI response suggestions.
+
+
+
Server: Online
+
AI: {ai_text}
+
Run command: {run_cmd_text}
+
+
+ How to use
+
+ - Download the extension zip from
/download/chrome-extension.zip (or copy chrome_screenshot_ext manually).
+ - Open
chrome://extensions in Chrome and enable Developer mode.
+ - Click Load unpacked and choose the
chrome_screenshot_ext folder.
+ - Open any page, run the extension, and send a screenshot to this server.
+
+
+ Need JSON health output? Use /health.
+
+
+
+"""
+
def do_GET(self): # noqa: N802
- if self.path not in ("/", "/health"):
- self._send_json(404, {"ok": False, "error": "not_found"})
+ parsed = urlparse(self.path)
+ path = parsed.path or "/"
+
+ if path == "/":
+ # Keep root JSON-compatible for extension and existing integrations.
+ self._send_json(200, self._health_payload())
return
- self._send_json(
- 200,
- {
- "ok": True,
- "service": "local_screenshot_bridge",
- "out_dir": str(self.server.out_dir), # type: ignore[attr-defined]
- "has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined]
- "ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined]
- },
- )
+
+ if path in ("/guide", "/docs"):
+ self._send_html(200, self._home_html())
+ return
+
+ if path in ("/download/chrome-extension.zip", "/chrome_screenshot_ext.zip"):
+ zip_bytes, err = self._build_extension_zip()
+ if err:
+ _log(self.server, "error", f"Extension zip failed: {err}") # type: ignore[arg-type]
+ self._send_json(500, {"ok": False, "error": "extension_zip_failed", "detail": err})
+ return
+ self._send_bytes(
+ 200,
+ zip_bytes or b"",
+ "application/zip",
+ 'attachment; filename="chrome_screenshot_ext.zip"',
+ )
+ return
+
+ if path == "/health":
+ self._send_json(200, self._health_payload())
+ return
+
+ self._send_json(404, {"ok": False, "error": "not_found"})
def do_OPTIONS(self): # noqa: N802
self.send_response(204)