cc-sidepanel: Brave/Chrome MV3 side-panel extension for a live Claude Code seat

This commit is contained in:
2026-09-11 01:29:42 +08:00
commit 8d64c86b10
8 changed files with 498 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
cc-sidepanel bridge — localhost HTTP shim so a browser extension
(no unix-socket access) can talk to injectd.py's unix socket.
Endpoints (GET/POST, JSON):
GET /seats -> injectd {op: seats}
GET /screen?seat=X&lines=60 -> injectd {op: screen}
POST /inject {seat, text} -> injectd {op: inject}
CORS is locked to chrome-extension:// origins only (set via env
CCSIDEPANEL_EXT_ID, no wildcard fallback — error loud if unset).
"""
import json
import os
import socket
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs
INJECTD_SOCK = "/tmp/injectd.sock"
PORT = int(os.environ.get("CCSIDEPANEL_PORT", "8988"))
EXT_ID = os.environ.get("CCSIDEPANEL_EXT_ID")
if not EXT_ID:
print("CCSIDEPANEL_EXT_ID not set — refusing to guess an allowed origin. "
"Load the unpacked extension, copy its ID from brave://extensions, "
"export CCSIDEPANEL_EXT_ID=<id> before starting bridge.py.", file=sys.stderr)
sys.exit(1)
ALLOWED_ORIGIN = "chrome-extension://{}".format(EXT_ID)
def injectd_call(req):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
s.connect(INJECTD_SOCK)
except (FileNotFoundError, ConnectionRefusedError) as exc:
return {"ok": False, "error": "injectd not reachable at {}: {}".format(INJECTD_SOCK, exc)}
s.sendall((json.dumps(req) + "\n").encode())
data = b""
while not data.endswith(b"\n"):
chunk = s.recv(65536)
if not chunk:
break
data += chunk
s.close()
return json.loads(data.decode())
class Handler(BaseHTTPRequestHandler):
def _cors(self):
self.send_header("Access-Control-Allow-Origin", ALLOWED_ORIGIN)
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type")
def _json(self, obj, status=200):
body = json.dumps(obj).encode()
self.send_response(status)
self._cors()
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_OPTIONS(self):
self.send_response(204)
self._cors()
self.end_headers()
def do_GET(self):
parsed = urlparse(self.path)
qs = parse_qs(parsed.query)
if parsed.path == "/seats":
return self._json(injectd_call({"op": "seats"}))
if parsed.path == "/screen":
seat = qs.get("seat", [None])[0]
if not seat:
return self._json({"ok": False, "error": "seat required"}, 400)
lines = int(qs.get("lines", ["60"])[0])
return self._json(injectd_call({"op": "screen", "seat": seat, "lines": lines}))
self._json({"ok": False, "error": "unknown path"}, 404)
def do_POST(self):
parsed = urlparse(self.path)
length = int(self.headers.get("Content-Length") or 0)
raw = self.rfile.read(length) if length else b"{}"
try:
payload = json.loads(raw.decode())
except json.JSONDecodeError:
return self._json({"ok": False, "error": "bad json"}, 400)
if parsed.path == "/inject":
seat, text = payload.get("seat"), payload.get("text")
if not seat or not text:
return self._json({"ok": False, "error": "seat and text required"}, 400)
return self._json(injectd_call({"op": "inject", "seat": seat, "text": text}))
self._json({"ok": False, "error": "unknown path"}, 404)
def log_message(self, fmt, *args):
pass # keep stdout quiet; run under a real logger if this graduates
if __name__ == "__main__":
print("cc-sidepanel bridge on :{} -> {} (allowed origin: {})".format(
PORT, INJECTD_SOCK, ALLOWED_ORIGIN))
ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()