// 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() }