Sesión claude caliente y persistente. Mantiene contexto entre mensajes (pruébalo: pregunta algo y luego refiérete a ello). Cada respuesta llega del SSE de la red, exacta, en ~2-3s. "Nueva conversación" reinicia la sesión sin memoria.
-
-
-
-
-
Un único daemon claude_session vivo detrás. Mensajes secuenciales. La memoria persiste hasta que reinicies.
-
-
-
-
diff --git a/playground/web/server.go b/playground/web/server.go
deleted file mode 100644
index 283e93d..0000000
--- a/playground/web/server.go
+++ /dev/null
@@ -1,198 +0,0 @@
-// Command server is a browser chat over a single hot claude_session daemon. Unlike
-// the claude_pipe/claude_wire chats (which spawn a fresh one-shot process per
-// message), this server launches ONE claude_session at boot and keeps it alive, so
-// the conversation has memory across turns and each message answers in ~2-3s.
-//
-// Per message it writes {"cmd":"send","prompt":...} to the daemon's stdin and
-// relays the daemon's NDJSON (text_delta + result) to the browser as SSE. A
-// /restart endpoint sends {"cmd":"restart"} to start a fresh conversation.
-//
-// The daemon's stdout is read only while holding a mutex, so requests are
-// serialized (a chat is sequential anyway).
-//
-// Run:
-//
-// cd apps/claude_session && go build -o claude_session .
-// cd playground && go run ./web # http://localhost:8101
-package main
-
-import (
- "bufio"
- _ "embed"
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "log"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "sync"
- "time"
-)
-
-//go:embed index.html
-var indexHTML []byte
-
-type daemonEvent struct {
- Type string `json:"type"`
- Text string `json:"text"`
- Result string `json:"result"`
- Message string `json:"message"`
-}
-
-var (
- mu sync.Mutex
- stdin io.Writer
- stdout *bufio.Scanner
- session *exec.Cmd
-)
-
-func main() {
- port := flag.String("port", "8101", "port to listen on")
- bin := flag.String("session", "/home/enmanuel/fn_registry/apps/claude_session/claude_session", "claude_session binary")
- cwd := flag.String("cwd", "/home/enmanuel/fn_registry", "cwd for the claude session")
- flag.Parse()
-
- abs, err := filepath.Abs(*bin)
- if err != nil || !fileExists(abs) {
- log.Fatalf("claude_session binary not found at %s — build it first (cd .. && go build -o claude_session .)", abs)
- }
-
- if err := startDaemon(abs, *cwd); err != nil {
- log.Fatalf("start daemon: %v", err)
- }
-
- http.HandleFunc("/", handleIndex)
- http.HandleFunc("/chat", handleChat)
- http.HandleFunc("/restart", handleRestart)
-
- log.Printf("claude_session web chat → http://localhost:%s (daemon=%s cwd=%s)", *port, abs, *cwd)
- log.Fatal(http.ListenAndServe(":"+*port, nil))
-}
-
-// startDaemon launches claude_session and blocks until its initial {"type":"ready"}.
-func startDaemon(bin, cwd string) error {
- cmd := exec.Command(bin, "--cwd", cwd)
- cmd.Stderr = os.Stderr
- in, err := cmd.StdinPipe()
- if err != nil {
- return err
- }
- out, err := cmd.StdoutPipe()
- if err != nil {
- return err
- }
- if err := cmd.Start(); err != nil {
- return err
- }
- sc := bufio.NewScanner(out)
- sc.Buffer(make([]byte, 1024*1024), 1024*1024)
- stdin, stdout, session = in, sc, cmd
-
- log.Printf("waiting for claude session to warm up...")
- for sc.Scan() {
- var ev daemonEvent
- if json.Unmarshal(sc.Bytes(), &ev) == nil && ev.Type == "ready" {
- log.Printf("claude session ready")
- return nil
- }
- }
- return fmt.Errorf("daemon exited before ready")
-}
-
-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 sends one prompt to the live daemon and relays its events 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("X-Accel-Buffering", "no")
-
- mu.Lock()
- defer mu.Unlock()
-
- cmd, _ := json.Marshal(map[string]string{"cmd": "send", "prompt": prompt})
- if _, err := fmt.Fprintln(stdin, string(cmd)); err != nil {
- sse(w, flusher, "error", fmt.Sprintf(`{"message":%q}`, err.Error()))
- return
- }
-
- // Relay daemon events until result, then consume the trailing ready.
- gotResult := false
- for stdout.Scan() {
- line := stdout.Bytes()
- var ev daemonEvent
- if json.Unmarshal(line, &ev) != nil {
- continue
- }
- switch ev.Type {
- case "text_delta", "result", "error":
- sse(w, flusher, "", string(line))
- if ev.Type == "result" || ev.Type == "error" {
- gotResult = true
- }
- case "ready":
- if gotResult {
- sse(w, flusher, "done", "{}")
- return
- }
- }
- }
- sse(w, flusher, "error", `{"message":"daemon stream ended"}`)
-}
-
-// handleRestart tells the daemon to start a fresh conversation.
-func handleRestart(w http.ResponseWriter, r *http.Request) {
- mu.Lock()
- defer mu.Unlock()
-
- if _, err := fmt.Fprintln(stdin, `{"cmd":"restart"}`); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- deadline := time.Now().Add(30 * time.Second)
- for stdout.Scan() {
- var ev daemonEvent
- if json.Unmarshal(stdout.Bytes(), &ev) == nil && ev.Type == "ready" {
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"ok":true}`))
- return
- }
- if time.Now().After(deadline) {
- break
- }
- }
- http.Error(w, "restart timeout", http.StatusGatewayTimeout)
-}
-
-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()
-}
-
-func fileExists(p string) bool {
- _, err := os.Stat(p)
- return err == nil
-}