From ed78bdb81acd9dc10b49ef26a2872dafd8babc7a Mon Sep 17 00:00:00 2001 From: agent Date: Thu, 4 Jun 2026 00:30:52 +0200 Subject: [PATCH] feat(playground): chat web sobre claude_wire (SSE interceptado de la red) Backend Go (web/server.go) con SSE que por cada mensaje lanza claude_wire y reenvia su NDJSON (text_delta + result) al navegador. Frontend vanilla (web/index.html). Mensajes serializados (mutex: claude_wire usa puerto mitmproxy fijo). Validado end-to-end: la respuesta llega token a token del SSE real (O, K -> OK), sin parsear el render. ~9s por mensaje vs ~15s del chat de claude_pipe. Puerto 8100. --- playground/README.md | 48 ++++++++++++ playground/web/go.mod | 3 + playground/web/index.html | 159 ++++++++++++++++++++++++++++++++++++++ playground/web/server.go | 123 +++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 playground/README.md create mode 100644 playground/web/go.mod create mode 100644 playground/web/index.html create mode 100644 playground/web/server.go diff --git a/playground/README.md b/playground/README.md new file mode 100644 index 0000000..5cc7e8c --- /dev/null +++ b/playground/README.md @@ -0,0 +1,48 @@ +# claude_wire — chat web (playground) + +Chat en el navegador sobre `claude_wire`: el texto de la respuesta se obtiene +interceptando el SSE del modelo en la red (no parseando el render de la terminal). + +Backend Go con SSE (`web/server.go`) que, por cada mensaje, lanza `claude_wire` +y reenvía su NDJSON (`text_delta` + `result`) al navegador. Frontend vanilla +(`web/index.html`), sin frameworks ni node_modules. No se indexa (playground del +padre). + +## Prerrequisitos + +- `mitmproxy` instalado (`mitmdump` en el PATH) + CA generada + (`~/.mitmproxy/mitmproxy-ca-cert.pem`). +- Binarios compilados: + - `apps/claude_pipe/claude_pipe` (driver de la TUI). + - `apps/claude_wire/claude_wire` (runner que intercepta el SSE). + +```bash +cd apps/claude_pipe && CGO_ENABLED=1 go build -tags fts5 -o claude_pipe . +cd ../claude_wire && go build -o claude_wire . +``` + +## Lanzar + +```bash +cd apps/claude_wire/playground +go run ./web # http://localhost:8100 +# o con flags: +go run ./web --port 8100 --root /home/enmanuel/fn_registry --wire ../claude_wire +``` + +Abre `http://localhost:8100` y escribe. Cada mensaje dirige la TUI interactiva de +claude por un mitmproxy y lee la respuesta del SSE de `api.anthropic.com`, token a +token, exacta. Corta en `message_stop` (sin idle ciego): ~9s por respuesta. + +## vs el chat de claude_pipe + +| | `claude_pipe` chat (`:8099`) | `claude_wire` chat (`:8100`) | +|---|---|---| +| Texto | parseado del render (heurístico) | exacto del SSE de la red | +| Streaming | snapshots del grid | token real | +| Latencia | ~15s | ~9s | + +## Notas + +- Mensajes serializados (un `claude_wire` a la vez: usa un puerto mitmproxy fijo). +- Sin memoria entre turnos (cada mensaje es una sesión claude one-shot). diff --git a/playground/web/go.mod b/playground/web/go.mod new file mode 100644 index 0000000..f856045 --- /dev/null +++ b/playground/web/go.mod @@ -0,0 +1,3 @@ +module cw_web + +go 1.25.0 diff --git a/playground/web/index.html b/playground/web/index.html new file mode 100644 index 0000000..0e11e3a --- /dev/null +++ b/playground/web/index.html @@ -0,0 +1,159 @@ + + + + + +claude_wire · chat (SSE interceptado) + + + +
+ +

claude_wire · chat

+ texto exacto del modelo, interceptado del SSE de la red +
+ +
+
+
sistema
Escribe un mensaje. Cada turno dirige la TUI interactiva de claude por un mitmproxy y lee la respuesta DEL CABLE (el SSE de api.anthropic.com), no de la pantalla. Texto exacto, token a token, sin memoria entre mensajes. +
+
+ +
+ + +
+
Cada mensaje = captura del SSE via mitmproxy. Corta en message_stop (sin idle ciego). ~9s por respuesta; el grueso es el arranque de la TUI.
+ + + + diff --git a/playground/web/server.go b/playground/web/server.go new file mode 100644 index 0000000..3196a1a --- /dev/null +++ b/playground/web/server.go @@ -0,0 +1,123 @@ +// Command server is a browser chat over claude_wire — the network-intercept path. +// For each message it launches claude_wire, which drives the interactive claude TUI +// through a mitmproxy and reads the model's SSE off the wire, then relays the +// resulting NDJSON (text_delta + result) to the browser as Server-Sent Events. +// +// Versus the claude_pipe chat (which parses the terminal render), this one shows the +// exact model text token by token and finishes as soon as the model's message_stop +// arrives — no blind idle wait. ~9s vs ~15s per message. +// +// claude_wire uses a fixed mitmproxy port, so messages are serialized with a mutex +// (a chat is sequential anyway: you wait for the reply before sending the next). +// +// Each message is an independent one-shot claude session (no memory across turns). +// +// Run: +// +// cd apps/claude_pipe && CGO_ENABLED=1 go build -tags fts5 -o claude_pipe . # the TUI driver +// cd ../claude_wire && go build -o claude_wire . # the runner +// cd playground && go run ./web # http://localhost:8100 +package main + +import ( + "bufio" + _ "embed" + "flag" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "sync" +) + +//go:embed index.html +var indexHTML []byte + +var ( + wireBin string + root string + one sync.Mutex // serialize: one claude_wire at a time (fixed proxy port) +) + +func main() { + port := flag.String("port", "8100", "port to listen on") + bin := flag.String("wire", "/home/enmanuel/fn_registry/apps/claude_wire/claude_wire", "path to the claude_wire binary") + rootDir := flag.String("root", "/home/enmanuel/fn_registry", "cwd for claude (MCP-approved repo)") + flag.Parse() + + abs, err := filepath.Abs(*bin) + if err != nil { + log.Fatalf("resolve --wire: %v", err) + } + if _, err := os.Stat(abs); err != nil { + log.Fatalf("claude_wire binary not found at %s — build it first:\n (cd .. && go build -o claude_wire .)", abs) + } + wireBin, root = abs, *rootDir + + http.HandleFunc("/", handleIndex) + http.HandleFunc("/chat", handleChat) + + log.Printf("claude_wire web chat → http://localhost:%s (wire=%s root=%s)", *port, wireBin, root) + log.Fatal(http.ListenAndServe(":"+*port, nil)) +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(indexHTML) +} + +// handleChat runs claude_wire for the prompt and relays its NDJSON as SSE. +func handleChat(w http.ResponseWriter, r *http.Request) { + prompt := r.URL.Query().Get("prompt") + if prompt == "" { + http.Error(w, "missing prompt", http.StatusBadRequest) + return + } + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming unsupported", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + + // Serialize: claude_wire binds a fixed mitmproxy port. + one.Lock() + defer one.Unlock() + + cmd := exec.CommandContext(r.Context(), wireBin, "--prompt", prompt, "--cwd", root) + stdout, err := cmd.StdoutPipe() + if err != nil { + sse(w, flusher, "error", fmt.Sprintf(`{"message":%q}`, err.Error())) + return + } + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + sse(w, flusher, "error", fmt.Sprintf(`{"message":%q}`, err.Error())) + return + } + + sc := bufio.NewScanner(stdout) + sc.Buffer(make([]byte, 1024*1024), 1024*1024) + for sc.Scan() { + sse(w, flusher, "", sc.Text()) + } + _ = cmd.Wait() + sse(w, flusher, "done", "{}") +} + +func sse(w http.ResponseWriter, f http.Flusher, event, data string) { + if event != "" { + fmt.Fprintf(w, "event: %s\n", event) + } + fmt.Fprintf(w, "data: %s\n\n", data) + f.Flush() +}