ed78bdb81a
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.
124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
// 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()
|
|
}
|